Skip to content

Commit 10e4fd4

Browse files
committed
8230526: jdk.internal.net.http.PlainProxyConnection is never reused by HttpClient
Fixed the PlainProxyConnection lookup key. Reviewed-by: chegar
1 parent 10f1f10 commit 10e4fd4

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ synchronized HttpConnection getConnection(boolean secure,
138138
InetSocketAddress addr,
139139
InetSocketAddress proxy) {
140140
if (stopped) return null;
141+
// for plain (unsecure) proxy connection the destination address is irrelevant.
142+
addr = secure || proxy == null ? addr : null;
141143
CacheKey key = new CacheKey(addr, proxy);
142144
HttpConnection c = secure ? findConnection(key, sslPool)
143145
: findConnection(key, plainPool);
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* Copyright (c) 2019, 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 com.sun.net.httpserver.HttpContext;
25+
import com.sun.net.httpserver.HttpExchange;
26+
import com.sun.net.httpserver.HttpHandler;
27+
import com.sun.net.httpserver.HttpServer;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.net.HttpURLConnection;
31+
import java.net.InetAddress;
32+
import java.net.InetSocketAddress;
33+
import java.net.Proxy;
34+
import java.net.ProxySelector;
35+
import java.net.SocketAddress;
36+
import java.net.URI;
37+
import java.net.URISyntaxException;
38+
import java.nio.charset.StandardCharsets;
39+
import java.security.NoSuchAlgorithmException;
40+
import java.util.List;
41+
import java.util.Set;
42+
import java.util.concurrent.ConcurrentLinkedQueue;
43+
import java.net.http.HttpClient;
44+
import java.net.http.HttpRequest;
45+
import java.net.http.HttpResponse;
46+
import java.util.stream.Collectors;
47+
48+
/**
49+
* @test
50+
* @bug 8230526
51+
* @summary Verifies that PlainProxyConnections are cached and reused properly. We do this by
52+
* verifying that the remote address of the HTTP exchange (on the fake proxy server)
53+
* is always the same InetSocketAddress.
54+
* @modules jdk.httpserver
55+
* @run main/othervm PlainProxyConnectionTest
56+
* @author danielfuchs
57+
*/
58+
public class PlainProxyConnectionTest {
59+
60+
static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
61+
static final String PATH = "/foo/";
62+
static final ConcurrentLinkedQueue<InetSocketAddress> connections = new ConcurrentLinkedQueue<>();
63+
64+
// For convenience the server is used both as a plain server and as a plain proxy.
65+
// When used as a proxy, it serves the request itself instead of forwarding it
66+
// to the requested server.
67+
static HttpServer createHttpsServer() throws IOException, NoSuchAlgorithmException {
68+
HttpServer server = com.sun.net.httpserver.HttpServer.create();
69+
HttpContext context = server.createContext(PATH);
70+
context.setHandler(new HttpHandler() {
71+
@Override
72+
public void handle(HttpExchange he) throws IOException {
73+
connections.add(he.getRemoteAddress());
74+
he.getResponseHeaders().add("encoding", "UTF-8");
75+
byte[] bytes = RESPONSE.getBytes(StandardCharsets.UTF_8);
76+
he.sendResponseHeaders(200, bytes.length > 0 ? bytes.length : -1);
77+
he.getResponseBody().write(bytes);
78+
he.close();
79+
}
80+
});
81+
82+
InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
83+
server.bind(addr, 0);
84+
return server;
85+
}
86+
87+
public static void main(String[] args)
88+
throws IOException,
89+
URISyntaxException,
90+
NoSuchAlgorithmException,
91+
InterruptedException
92+
{
93+
HttpServer server = createHttpsServer();
94+
server.start();
95+
try {
96+
test(server, HttpClient.Version.HTTP_1_1);
97+
test(server, HttpClient.Version.HTTP_2);
98+
} finally {
99+
server.stop(0);
100+
System.out.println("Server stopped");
101+
}
102+
}
103+
104+
/**
105+
* A Proxy Selector that wraps a ProxySelector.of(), and counts the number
106+
* of times its select method has been invoked. This can be used to ensure
107+
* that the Proxy Selector is invoked only once per HttpClient.sendXXX
108+
* invocation.
109+
*/
110+
static class CountingProxySelector extends ProxySelector {
111+
private final ProxySelector proxySelector;
112+
private volatile int count; // 0
113+
private CountingProxySelector(InetSocketAddress proxyAddress) {
114+
proxySelector = ProxySelector.of(proxyAddress);
115+
}
116+
117+
public static CountingProxySelector of(InetSocketAddress proxyAddress) {
118+
return new CountingProxySelector(proxyAddress);
119+
}
120+
121+
int count() { return count; }
122+
123+
@Override
124+
public List<Proxy> select(URI uri) {
125+
count++;
126+
return proxySelector.select(uri);
127+
}
128+
129+
@Override
130+
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
131+
proxySelector.connectFailed(uri, sa, ioe);
132+
}
133+
}
134+
135+
// The sanity test sends request to the server, and through the proxy,
136+
// using the legacy HttpURLConnection to verify that server and proxy
137+
// work as expected.
138+
private static void performSanityTest(HttpServer server, URI uri, URI proxiedURI)
139+
throws IOException {
140+
connections.clear();
141+
System.out.println("Verifying communication with server");
142+
try (InputStream is = uri.toURL().openConnection().getInputStream()) {
143+
String resp = new String(is.readAllBytes(), StandardCharsets.UTF_8);
144+
System.out.println(resp);
145+
if (!RESPONSE.equals(resp)) {
146+
throw new AssertionError("Unexpected response from server");
147+
}
148+
}
149+
System.out.println("Communication with server OK");
150+
int count = connections.size();
151+
if (count != 1) {
152+
System.err.println("Unexpected connection count: " + count);
153+
System.err.println("Connections: " + connections);
154+
throw new AssertionError("Expected only one connection: " + connections);
155+
}
156+
try {
157+
System.out.println("Pretending the server is a proxy...");
158+
Proxy p = new Proxy(Proxy.Type.HTTP,
159+
InetSocketAddress.createUnresolved(
160+
server.getAddress().getAddress().getHostAddress(),
161+
server.getAddress().getPort()));
162+
System.out.println("Verifying communication with proxy");
163+
HttpURLConnection conn = (HttpURLConnection) proxiedURI.toURL().openConnection(p);
164+
try (InputStream is = conn.getInputStream()) {
165+
String resp = new String(is.readAllBytes(), StandardCharsets.UTF_8);
166+
System.out.println(resp);
167+
if (!RESPONSE.equals(resp)) {
168+
throw new AssertionError("Unexpected response from proxy");
169+
}
170+
}
171+
count = connections.size();
172+
if (count != 2) {
173+
System.err.println("Unexpected connection count: " + count);
174+
System.err.println("Connections: " + connections);
175+
throw new AssertionError("Expected two connection: " + connections);
176+
}
177+
System.out.println("Communication with proxy OK");
178+
} finally {
179+
connections.clear();
180+
}
181+
}
182+
183+
public static void test(HttpServer server, HttpClient.Version version)
184+
throws IOException,
185+
URISyntaxException,
186+
InterruptedException {
187+
connections.clear();
188+
System.out.println("\n===== Testing with " + version);
189+
System.out.println("Server is: " + server.getAddress().toString());
190+
URI uri = new URI("http", null,
191+
server.getAddress().getAddress().getHostAddress(),
192+
server.getAddress().getPort(), PATH + "x",
193+
null, null);
194+
URI proxiedURI = new URI("http://some.host.that.does.not.exist:4242" + PATH + "x");
195+
196+
performSanityTest(server, uri, proxiedURI);
197+
198+
try {
199+
connections.clear();
200+
System.out.println("\nReal test begins here.");
201+
System.out.println("Setting up request with HttpClient for version: "
202+
+ version.name());
203+
// This will force the HTTP client to see the server as a proxy,
204+
// and to (re)use a PlainProxyConnection to send the request
205+
// to the fake `proxiedURI` at
206+
// http://some.host.that.does.not.exist:4242/foo/x
207+
//
208+
CountingProxySelector ps = CountingProxySelector.of(
209+
InetSocketAddress.createUnresolved(
210+
server.getAddress().getAddress().getHostAddress(),
211+
server.getAddress().getPort()));
212+
HttpClient client = HttpClient.newBuilder()
213+
.version(version)
214+
.proxy(ps)
215+
.build();
216+
HttpRequest request = HttpRequest.newBuilder()
217+
.uri(proxiedURI)
218+
.GET()
219+
.build();
220+
221+
System.out.println("Sending request with HttpClient: " + request);
222+
HttpResponse<String> response
223+
= client.send(request, HttpResponse.BodyHandlers.ofString());
224+
System.out.println("Got response");
225+
String resp = response.body();
226+
System.out.println("Received: " + resp);
227+
if (!RESPONSE.equals(resp)) {
228+
throw new AssertionError("Unexpected response");
229+
}
230+
if (ps.count() > 1) {
231+
throw new AssertionError("CountingProxySelector. Expected 1, got " + ps.count());
232+
}
233+
int count = connections.size();
234+
if (count != 1) {
235+
System.err.println("Unexpected connection count: " + count);
236+
System.err.println("Connections: " + connections);
237+
throw new AssertionError("Expected only one connection: " + connections);
238+
}
239+
for (int i = 2; i < 5; i++) {
240+
System.out.println("Sending next request (" + i + ") with HttpClient: " + request);
241+
response = client.send(request, HttpResponse.BodyHandlers.ofString());
242+
System.out.println("Got response");
243+
resp = response.body();
244+
System.out.println("Received: " + resp);
245+
if (!RESPONSE.equals(resp)) {
246+
throw new AssertionError("Unexpected response");
247+
}
248+
if (ps.count() > i) {
249+
throw new AssertionError("CountingProxySelector. Expected "
250+
+ i + ", got " + ps.count());
251+
}
252+
count = connections.size();
253+
if (count != i) {
254+
System.err.println("Unexpected connection count: " + count);
255+
System.err.println("Connections: " + connections);
256+
throw new AssertionError("Expected " + i + ": " + connections);
257+
}
258+
}
259+
Set<InetSocketAddress> remote = connections.stream().distinct().collect(Collectors.toSet());
260+
count = remote.size();
261+
if (count != 1) {
262+
System.err.println("Unexpected connection count: " + count);
263+
System.err.println("Connections: " + remote);
264+
throw new AssertionError("Expected only one connection: " + remote);
265+
} else {
266+
System.out.println("PASSED: Proxy received only one connection from: " + remote);
267+
}
268+
} finally {
269+
connections.clear();
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)