Skip to content

Commit 4395668

Browse files
jaikiranPaul Hohensee
authored andcommitted
8305906: HttpClient may use incorrect key when finding pooled HTTP/2 connection for IPv6 address
Reviewed-by: phh Backport-of: 3ccb3c0e09f9a414229d3f76031f3fc8f271c936
1 parent 200c2a0 commit 4395668

File tree

4 files changed

+227
-12
lines changed

4 files changed

+227
-12
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ class Http2ClientImpl {
9393
*/
9494
CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
9595
Exchange<?> exchange) {
96-
URI uri = req.uri();
97-
InetSocketAddress proxy = req.proxy();
98-
String key = Http2Connection.keyFor(uri, proxy);
96+
String key = Http2Connection.keyFor(req);
9997

10098
synchronized (this) {
10199
Http2Connection connection = connections.get(key);

src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ private Http2Connection(HttpRequestImpl request,
402402
this(connection,
403403
h2client,
404404
1,
405-
keyFor(request.uri(), request.proxy()));
405+
keyFor(request));
406406

407407
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
408408

@@ -534,15 +534,13 @@ static String keyFor(HttpConnection connection) {
534534
return keyString(isSecure, proxyAddr, addr.getHostString(), addr.getPort());
535535
}
536536

537-
static String keyFor(URI uri, InetSocketAddress proxy) {
538-
boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
539-
540-
String host = uri.getHost();
541-
int port = uri.getPort();
542-
return keyString(isSecure, proxy, host, port);
537+
static String keyFor(final HttpRequestImpl request) {
538+
final InetSocketAddress targetAddr = request.getAddress();
539+
final InetSocketAddress proxy = request.proxy();
540+
final boolean secure = request.secure();
541+
return keyString(secure, proxy, targetAddr.getHostString(), targetAddr.getPort());
543542
}
544543

545-
546544
// Compute the key for an HttpConnection in the Http2ClientImpl pool:
547545
// The key string follows one of the three forms below:
548546
// {C,S}:H:host:port

test/jdk/java/net/httpclient/HttpServerAdapters.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -227,6 +227,7 @@ public static abstract class HttpTestExchange implements AutoCloseable {
227227
public abstract URI getRequestURI();
228228
public abstract String getRequestMethod();
229229
public abstract void close();
230+
public abstract InetSocketAddress getRemoteAddress();
230231
public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
231232
ByteArrayInputStream bais = new ByteArrayInputStream(body);
232233
serverPush(uri, headers, bais);
@@ -284,6 +285,12 @@ void doFilter(Filter.Chain chain) throws IOException {
284285
}
285286
@Override
286287
public void close() { exchange.close(); }
288+
289+
@Override
290+
public InetSocketAddress getRemoteAddress() {
291+
return exchange.getRemoteAddress();
292+
}
293+
287294
@Override
288295
public URI getRequestURI() { return exchange.getRequestURI(); }
289296
@Override
@@ -339,6 +346,12 @@ void doFilter(Filter.Chain filter) throws IOException {
339346
}
340347
@Override
341348
public void close() { exchange.close();}
349+
350+
@Override
351+
public InetSocketAddress getRemoteAddress() {
352+
return exchange.getRemoteAddress();
353+
}
354+
342355
@Override
343356
public URI getRequestURI() { return exchange.getRequestURI(); }
344357
@Override
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import java.io.IOException;
25+
import java.io.OutputStream;
26+
import java.net.InetSocketAddress;
27+
import java.net.URI;
28+
import java.net.http.HttpClient;
29+
import java.net.http.HttpRequest;
30+
import java.net.http.HttpResponse;
31+
import java.net.http.HttpResponse.BodyHandlers;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.ArrayList;
34+
import java.util.List;
35+
import java.util.stream.Stream;
36+
37+
import javax.net.ssl.SSLContext;
38+
39+
import jdk.test.lib.net.IPSupport;
40+
import jdk.test.lib.net.SimpleSSLContext;
41+
import org.junit.jupiter.api.AfterAll;
42+
import org.junit.jupiter.api.Assumptions;
43+
import org.junit.jupiter.api.BeforeAll;
44+
import org.junit.jupiter.params.ParameterizedTest;
45+
import org.junit.jupiter.params.provider.Arguments;
46+
import org.junit.jupiter.params.provider.MethodSource;
47+
import static java.net.http.HttpClient.Builder.NO_PROXY;
48+
import static java.net.http.HttpClient.Version.HTTP_2;
49+
import static org.junit.jupiter.api.Assertions.assertEquals;
50+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
51+
import static org.junit.jupiter.api.Assertions.assertNotNull;
52+
53+
/*
54+
* @test
55+
* @bug 8305906
56+
* @summary verify that the HttpClient pools and reuses a connection for HTTP/2 requests
57+
* @library /test/lib server/ ../
58+
* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters
59+
* ReferenceTracker jdk.test.lib.net.IPSupport
60+
* @modules java.net.http/jdk.internal.net.http.common
61+
* java.net.http/jdk.internal.net.http.frame
62+
* java.net.http/jdk.internal.net.http.hpack
63+
* java.logging
64+
* java.base/sun.net.www.http
65+
* java.base/sun.net.www
66+
* java.base/sun.net
67+
*
68+
* @run junit/othervm ConnectionReuseTest
69+
* @run junit/othervm -Djava.net.preferIPv6Addresses=true ConnectionReuseTest
70+
*/
71+
public class ConnectionReuseTest implements HttpServerAdapters {
72+
73+
private static SSLContext sslContext;
74+
private static HttpTestServer http2_Server; // h2 server over HTTP
75+
private static HttpTestServer https2_Server; // h2 server over HTTPS
76+
77+
@BeforeAll
78+
public static void beforeAll() throws Exception {
79+
if (IPSupport.preferIPv6Addresses()) {
80+
IPSupport.printPlatformSupport(System.err); // for debug purposes
81+
// this test is run with -Djava.net.preferIPv6Addresses=true, so skip (all) tests
82+
// if IPv6 isn't supported on this host
83+
Assumptions.assumeTrue(IPSupport.hasIPv6(), "Skipping tests - IPv6 is not supported");
84+
}
85+
sslContext = new SimpleSSLContext().get();
86+
assertNotNull(sslContext, "Unexpected null sslContext");
87+
88+
http2_Server = HttpTestServer.of(
89+
new Http2TestServer("localhost", false, 0));
90+
http2_Server.addHandler(new Handler(), "/");
91+
http2_Server.start();
92+
System.out.println("Started HTTP v2 server at " + http2_Server.serverAuthority());
93+
94+
https2_Server = HttpTestServer.of(
95+
new Http2TestServer("localhost", true, sslContext));
96+
https2_Server.addHandler(new Handler(), "/");
97+
https2_Server.start();
98+
System.out.println("Started HTTPS v2 server at " + https2_Server.serverAuthority());
99+
}
100+
101+
@AfterAll
102+
public static void afterAll() {
103+
if (https2_Server != null) {
104+
System.out.println("Stopping server " + https2_Server);
105+
https2_Server.stop();
106+
}
107+
if (http2_Server != null) {
108+
System.out.println("Stopping server " + http2_Server);
109+
http2_Server.stop();
110+
}
111+
}
112+
113+
private static Stream<Arguments> requestURIs() throws Exception {
114+
final List<Arguments> arguments = new ArrayList<>();
115+
// h2 over HTTPS
116+
arguments.add(Arguments.of(new URI("https://" + https2_Server.serverAuthority() + "/")));
117+
// h2 over HTTP
118+
arguments.add(Arguments.of(new URI("http://" + http2_Server.serverAuthority() + "/")));
119+
if (IPSupport.preferIPv6Addresses()) {
120+
if (http2_Server.getAddress().getAddress().isLoopbackAddress()) {
121+
// h2 over HTTP, use the short form of the host, in the request URI
122+
arguments.add(Arguments.of(new URI("http://[::1]:" +
123+
http2_Server.getAddress().getPort() + "/")));
124+
}
125+
}
126+
return arguments.stream();
127+
}
128+
129+
/**
130+
* Uses a single instance of a HttpClient and issues multiple requests to {@code requestURI}
131+
* and expects that each of the request internally uses the same connection
132+
*/
133+
@ParameterizedTest
134+
@MethodSource("requestURIs")
135+
public void testConnReuse(final URI requestURI) throws Throwable {
136+
final HttpClient.Builder builder = HttpClient.newBuilder()
137+
.proxy(NO_PROXY).sslContext(sslContext);
138+
final HttpRequest req = HttpRequest.newBuilder().uri(requestURI)
139+
.GET().version(HTTP_2).build();
140+
final ReferenceTracker tracker = ReferenceTracker.INSTANCE;
141+
Throwable testFailure = null;
142+
HttpClient client = tracker.track(builder.build());
143+
try {
144+
String clientConnAddr = null;
145+
for (int i = 1; i <= 5; i++) {
146+
System.out.println("Issuing request(" + i + ") " + req);
147+
final HttpResponse<String> resp = client.send(req, BodyHandlers.ofString());
148+
assertEquals(200, resp.statusCode(), "unexpected response code");
149+
final String respBody = resp.body();
150+
System.out.println("Server side handler responded to a request from " + respBody);
151+
assertNotEquals(Handler.UNKNOWN_CLIENT_ADDR, respBody,
152+
"server handler couldn't determine client address in request");
153+
if (i == 1) {
154+
// for the first request we just keep track of the client connection address
155+
// that got used for this request
156+
clientConnAddr = respBody;
157+
} else {
158+
// verify that the client connection used to issue the request is the same
159+
// as the previous request's client connection
160+
assertEquals(clientConnAddr, respBody, "HttpClient unexpectedly used a" +
161+
" different connection for request(" + i + ")");
162+
}
163+
}
164+
} catch (Throwable t) {
165+
testFailure = t;
166+
} finally {
167+
// dereference the client to allow the tracker to verify the resources
168+
// have been released
169+
client = null;
170+
// wait for the client to be shutdown
171+
final AssertionError trackerFailure = tracker.check(2000);
172+
if (testFailure != null) {
173+
if (trackerFailure != null) {
174+
// add the failure reported by the tracker as a suppressed
175+
// exception and throw the original test failure
176+
testFailure.addSuppressed(trackerFailure);
177+
}
178+
throw testFailure;
179+
}
180+
if (trackerFailure != null) {
181+
// the test itself didn't fail but the tracker check failed.
182+
// fail the test with this exception
183+
throw trackerFailure;
184+
}
185+
}
186+
}
187+
188+
private static final class Handler implements HttpTestHandler {
189+
190+
private static final String UNKNOWN_CLIENT_ADDR = "unknown";
191+
192+
@Override
193+
public void handle(final HttpTestExchange t) throws IOException {
194+
final InetSocketAddress clientAddr = t.getRemoteAddress();
195+
System.out.println("Handling request " + t.getRequestURI() + " from " + clientAddr);
196+
// we write out the client address into the response body
197+
final byte[] responseBody = clientAddr == null
198+
? UNKNOWN_CLIENT_ADDR.getBytes(StandardCharsets.UTF_8)
199+
: clientAddr.toString().getBytes(StandardCharsets.UTF_8);
200+
t.sendResponseHeaders(200, responseBody.length);
201+
try (final OutputStream os = t.getResponseBody()) {
202+
os.write(responseBody);
203+
}
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)