Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport to 1.16: http: fixing a bug with IPv6 hosts #14238

Merged
merged 3 commits into from
Dec 3, 2020
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
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Bug Fixes
---------
*Changes expected to improve the state of the world and are unlikely to have negative effects*
* examples: examples use v3 configs.
* http: fixed URL parsing for HTTP/1.1 fully qualified URLs and connect requests containing IPv6 addresses.
* listener: fix crash when disabling or re-enabling listeners due to overload while processing LDS updates.
* proxy_proto: fixed a bug where the wrong downstream address got sent to upstream connections.
* proxy_proto: fixed a bug where network filters would not have the correct downstreamRemoteAddress() when accessed from the StreamInfo. This could result in incorrect enforcement of RBAC rules in the RBAC network filter (but not in the RBAC HTTP filter), or incorrect access log addresses from tcp_proxy.
Expand Down
47 changes: 39 additions & 8 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,30 @@ namespace Http {

static const char kDefaultPath[] = "/";

// If http_parser encounters an IP address [address] as the host it will set the offset and
// length to point to 'address' rather than '[address]'. Fix this by adjusting the offset
// and length to include the brackets.
// @param absolute_url the absolute URL. This is usually of the form // http://host/path
// but may be host:port for CONNECT requests
// @param offset the offset for the first character of the host. For IPv6 hosts
// this will point to the first character inside the brackets and will be
// adjusted to point at the brackets
// @param len the length of the host-and-port field. For IPv6 hosts this will
// not include the brackets and will be adjusted to do so.
bool maybeAdjustForIpv6(absl::string_view absolute_url, uint64_t& offset, uint64_t& len) {
// According to https://tools.ietf.org/html/rfc3986#section-3.2.2 the only way a hostname
// may begin with '[' is if it's an ipv6 address.
if (offset == 0 || *(absolute_url.data() + offset - 1) != '[') {
return false;
}
// Start one character sooner and end one character later.
offset--;
len += 2;
// HTTP parser ensures that any [ has a closing ]
ASSERT(absolute_url.length() >= offset + len);
return true;
}

bool Utility::Url::initialize(absl::string_view absolute_url, bool is_connect) {
struct http_parser_url u;
http_parser_url_init(&u);
Expand All @@ -244,20 +268,27 @@ bool Utility::Url::initialize(absl::string_view absolute_url, bool is_connect) {
scheme_ = absl::string_view(absolute_url.data() + u.field_data[UF_SCHEMA].off,
u.field_data[UF_SCHEMA].len);

uint16_t authority_len = u.field_data[UF_HOST].len;
uint64_t authority_len = u.field_data[UF_HOST].len;
if ((u.field_set & (1 << UF_PORT)) == (1 << UF_PORT)) {
authority_len = authority_len + u.field_data[UF_PORT].len + 1;
}
host_and_port_ =
absl::string_view(absolute_url.data() + u.field_data[UF_HOST].off, authority_len);

uint64_t authority_beginning = u.field_data[UF_HOST].off;
const bool is_ipv6 = maybeAdjustForIpv6(absolute_url, authority_beginning, authority_len);
host_and_port_ = absl::string_view(absolute_url.data() + authority_beginning, authority_len);
if (is_ipv6 && !parseAuthority(host_and_port_).is_ip_address_) {
return false;
}

// RFC allows the absolute-uri to not end in /, but the absolute path form
// must start with
uint64_t path_len = absolute_url.length() - (u.field_data[UF_HOST].off + hostAndPort().length());
if (path_len > 0) {
uint64_t path_beginning = u.field_data[UF_HOST].off + hostAndPort().length();
path_and_query_params_ = absl::string_view(absolute_url.data() + path_beginning, path_len);
// must start with. Determine if there's a non-zero path, and if so determine
// the length of the path, query params etc.
uint64_t path_etc_len = absolute_url.length() - (authority_beginning + hostAndPort().length());
if (path_etc_len > 0) {
uint64_t path_beginning = authority_beginning + hostAndPort().length();
path_and_query_params_ = absl::string_view(absolute_url.data() + path_beginning, path_etc_len);
} else if (!is_connect) {
ASSERT((u.field_set & (1 << UF_PATH)) == 0);
path_and_query_params_ = absl::string_view(kDefaultPath, 1);
}
return true;
Expand Down
32 changes: 28 additions & 4 deletions test/common/http/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,9 @@ TEST(Url, ParsingFails) {
EXPECT_FALSE(url.initialize("random_scheme://host.com/path", false));
EXPECT_FALSE(url.initialize("http://www.foo.com", true));
EXPECT_FALSE(url.initialize("foo.com", true));
EXPECT_FALSE(url.initialize("http://[notaddress]:80/?query=param", false));
EXPECT_FALSE(url.initialize("http://[1::z::2]:80/?query=param", false));
EXPECT_FALSE(url.initialize("http://1.2.3.4:65536/?query=param", false));
}

void validateUrl(absl::string_view raw_url, absl::string_view expected_scheme,
Expand All @@ -1262,12 +1265,17 @@ void validateUrl(absl::string_view raw_url, absl::string_view expected_scheme,
EXPECT_EQ(url.pathAndQueryParams(), expected_path);
}

void validateConnectUrl(absl::string_view raw_url, absl::string_view expected_host_port) {
void validateConnectUrl(absl::string_view raw_url) {
Utility::Url url;
ASSERT_TRUE(url.initialize(raw_url, true)) << "Failed to initialize " << raw_url;
EXPECT_TRUE(url.scheme().empty());
EXPECT_TRUE(url.pathAndQueryParams().empty());
EXPECT_EQ(url.hostAndPort(), expected_host_port);
EXPECT_EQ(url.hostAndPort(), raw_url);
}

void invalidConnectUrl(absl::string_view raw_url) {
Utility::Url url;
ASSERT_FALSE(url.initialize(raw_url, true)) << "Unexpectedly initialized " << raw_url;
}

TEST(Url, ParsingTest) {
Expand Down Expand Up @@ -1302,6 +1310,14 @@ TEST(Url, ParsingTest) {
validateUrl("http://www.host.com:80/?query=param", "http", "www.host.com:80", "/?query=param");
validateUrl("http://www.host.com/?query=param", "http", "www.host.com", "/?query=param");

// Test with an ipv4 host address.
validateUrl("http://1.2.3.4/?query=param", "http", "1.2.3.4", "/?query=param");
validateUrl("http://1.2.3.4:80/?query=param", "http", "1.2.3.4:80", "/?query=param");

// Test with an ipv6 address
validateUrl("http://[1::2:3]/?query=param", "http", "[1::2:3]", "/?query=param");
validateUrl("http://[1::2:3]:80/?query=param", "http", "[1::2:3]:80", "/?query=param");

// Test url with query parameter but without slash
validateUrl("http://www.host.com:80?query=param", "http", "www.host.com:80", "?query=param");
validateUrl("http://www.host.com?query=param", "http", "www.host.com", "?query=param");
Expand All @@ -1324,8 +1340,16 @@ TEST(Url, ParsingTest) {
}

TEST(Url, ParsingForConnectTest) {
validateConnectUrl("host.com:443", "host.com:443");
validateConnectUrl("host.com:80", "host.com:80");
validateConnectUrl("host.com:443");
validateConnectUrl("host.com:80");
validateConnectUrl("1.2.3.4:80");
validateConnectUrl("[1:2::3:4]:80");

invalidConnectUrl("[::12345678]:80");
invalidConnectUrl("[1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1]:80");
invalidConnectUrl("[1:1]:80");
invalidConnectUrl("[:::]:80");
invalidConnectUrl("[::1::]:80");
}

void validatePercentEncodingEncodeDecode(absl::string_view source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,7 @@ TEST_P(ProxyFilterIntegrationTest, UpstreamTlsWithIpHost) {
{":method", "POST"},
{":path", "/test/long/url"},
{":scheme", "http"},
{":authority", fmt::format("{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()),
fake_upstreams_[0]->localAddress()->ip()->port())}};
{":authority", fake_upstreams_[0]->localAddress()->asString()}};

auto response = codec_client_->makeHeaderOnlyRequest(request_headers);
waitForNextUpstreamRequest();
Expand Down
36 changes: 36 additions & 0 deletions test/integration/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,41 @@ TEST_P(IntegrationTest, AbsolutePath) {
EXPECT_FALSE(response.find("HTTP/1.1 404 Not Found\r\n") == 0);
}

// Make that both IPv4 and IPv6 hosts match when using relative and absolute URLs.
TEST_P(IntegrationTest, TestHostWithAddress) {
useAccessLog("%REQ(Host)%\n");
std::string address_string;
if (GetParam() == Network::Address::IpVersion::v4) {
address_string = TestUtility::getIpv4Loopback();
} else {
address_string = "[::1]";
}

auto host = config_helper_.createVirtualHost(address_string.c_str(), "/");
host.set_require_tls(envoy::config::route::v3::VirtualHost::ALL);
config_helper_.addVirtualHost(host);

initialize();
std::string response;

// Test absolute URL with ipv6.
sendRawHttpAndWaitForResponse(
lookupPort("http"), absl::StrCat("GET http://", address_string, " HTTP/1.1\r\n\r\n").c_str(),
&response, true);
EXPECT_FALSE(response.find("HTTP/1.1 404 Not Found\r\n") == 0);
EXPECT_TRUE(response.find("301") != std::string::npos);
EXPECT_THAT(waitForAccessLog(access_log_name_), HasSubstr(address_string));

// Test normal IPv6 request as well.
response.clear();
sendRawHttpAndWaitForResponse(
lookupPort("http"),
absl::StrCat("GET / HTTP/1.1\r\nHost: ", address_string, "\r\n\r\n").c_str(), &response,
true);
EXPECT_FALSE(response.find("HTTP/1.1 404 Not Found\r\n") == 0);
EXPECT_TRUE(response.find("301") != std::string::npos);
}

TEST_P(IntegrationTest, AbsolutePathWithPort) {
// Configure www.namewithport.com:1234 to send a redirect, and ensure the redirect is
// encountered via absolute URL with a port.
Expand All @@ -914,6 +949,7 @@ TEST_P(IntegrationTest, AbsolutePathWithPort) {
lookupPort("http"), "GET http://www.namewithport.com:1234 HTTP/1.1\r\nHost: host\r\n\r\n",
&response, true);
EXPECT_FALSE(response.find("HTTP/1.1 404 Not Found\r\n") == 0);
EXPECT_TRUE(response.find("301") != std::string::npos);
}

TEST_P(IntegrationTest, AbsolutePathWithoutPort) {
Expand Down