1 /*
  2  * Copyright (c) 2019, 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.net.*;
 26 import java.net.http.HttpClient;
 27 import java.net.http.HttpRequest;
 28 import java.net.http.HttpResponse;
 29 import java.nio.charset.StandardCharsets;
 30 import java.util.ArrayList;
 31 import java.util.List;
 32 import java.util.concurrent.*;
 33 import java.util.concurrent.atomic.AtomicLong;
 34 import jdk.httpclient.test.lib.common.HttpServerAdapters;
 35 
 36 import com.sun.net.httpserver.HttpsConfigurator;
 37 import com.sun.net.httpserver.HttpsServer;
 38 import jdk.httpclient.test.lib.common.TestServerConfigurator;
 39 import org.testng.annotations.AfterClass;
 40 import org.testng.annotations.BeforeClass;
 41 import org.testng.annotations.DataProvider;
 42 import org.testng.annotations.Test;
 43 
 44 import javax.net.ssl.SSLContext;
 45 
 46 import static java.net.http.HttpClient.Version.HTTP_1_1;
 47 import static java.net.http.HttpClient.Version.HTTP_2;
 48 
 49 /**
 50  * @test
 51  * @bug 8232853
 52  * @summary AuthenticationFilter.Cache::remove may throw ConcurrentModificationException
 53  * @library /test/lib /test/jdk/java/net/httpclient/lib
 54  * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext
 55  *        DigestEchoServer jdk.httpclient.test.lib.common.TestServerConfigurator
 56  * @run testng/othervm -Dtest.requiresHost=true
 57  * -Djdk.tracePinnedThreads=full
 58  * -Djdk.httpclient.HttpClient.log=headers
 59  * -Djdk.internal.httpclient.debug=false
 60  * AuthFilterCacheTest
 61  */
 62 
 63 public class AuthFilterCacheTest implements HttpServerAdapters {
 64 
 65     static final String RESPONSE_BODY = "Hello World!";
 66     static final int REQUEST_COUNT = 5;
 67     static final int URI_COUNT = 6;
 68     static final CyclicBarrier barrier = new CyclicBarrier(REQUEST_COUNT * URI_COUNT);
 69     static final SSLContext context;
 70 
 71     static {
 72         try {
 73             context = new jdk.test.lib.net.SimpleSSLContext().get();
 74             SSLContext.setDefault(context);
 75         } catch (Exception x) {
 76             throw new ExceptionInInitializerError(x);
 77         }
 78     }
 79 
 80     HttpTestServer http1Server;
 81     HttpTestServer http2Server;
 82     HttpTestServer https1Server;
 83     HttpTestServer https2Server;
 84     DigestEchoServer.TunnelingProxy proxy;
 85     URI http1URI;
 86     URI https1URI;
 87     URI http2URI;
 88     URI https2URI;
 89     InetSocketAddress proxyAddress;
 90     ProxySelector proxySelector;
 91     MyAuthenticator auth;
 92     HttpClient client;
 93     ExecutorService serverExecutor = Executors.newCachedThreadPool();
 94     ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
 95             .name("HttpClient-Worker", 0).factory());
 96 
 97     @DataProvider(name = "uris")
 98     Object[][] testURIs() {
 99         Object[][] uris = new Object[][]{
100                 {List.of(http1URI.resolve("direct/orig/"),
101                         https1URI.resolve("direct/orig/"),
102                         https1URI.resolve("proxy/orig/"),
103                         http2URI.resolve("direct/orig/"),
104                         https2URI.resolve("direct/orig/"),
105                         https2URI.resolve("proxy/orig/"))}
106         };
107         return uris;
108     }
109 
110     public HttpClient newHttpClient(ProxySelector ps, Authenticator auth) {
111         HttpClient.Builder builder = HttpClient
112                 .newBuilder()
113                 .executor(virtualExecutor)
114                 .sslContext(context)
115                 .authenticator(auth)
116                 .proxy(ps);
117         return builder.build();
118     }
119 
120     @BeforeClass
121     public void setUp() throws Exception {
122         try {
123             InetSocketAddress sa =
124                     new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
125             auth = new MyAuthenticator();
126 
127             // HTTP/1.1
128             http1Server = HttpTestServer.create(HTTP_1_1, null, serverExecutor);
129             http1Server.addHandler(new TestHandler(), "/AuthFilterCacheTest/http1/");
130             http1Server.start();
131             http1URI = new URI("http://" + http1Server.serverAuthority()
132                     + "/AuthFilterCacheTest/http1/");
133 
134             // HTTPS/1.1
135             HttpsServer sserver1 = HttpsServer.create(sa, 100);
136             sserver1.setExecutor(serverExecutor);
137             sserver1.setHttpsConfigurator(new TestServerConfigurator(sa.getAddress(), context));
138             https1Server = HttpTestServer.of(sserver1);
139             https1Server.addHandler(new TestHandler(), "/AuthFilterCacheTest/https1/");
140             https1Server.start();
141             https1URI = new URI("https://" + https1Server.serverAuthority()
142                     + "/AuthFilterCacheTest/https1/");
143 
144             // HTTP/2.0
145             http2Server = HttpTestServer.create(HTTP_2);
146             http2Server.addHandler(new TestHandler(), "/AuthFilterCacheTest/http2/");
147             http2Server.start();
148             http2URI = new URI("http://" + http2Server.serverAuthority()
149                     + "/AuthFilterCacheTest/http2/");
150 
151             // HTTPS/2.0
152             https2Server = HttpTestServer.create(HTTP_2, SSLContext.getDefault());
153             https2Server.addHandler(new TestHandler(), "/AuthFilterCacheTest/https2/");
154             https2Server.start();
155             https2URI = new URI("https://" + https2Server.serverAuthority()
156                     + "/AuthFilterCacheTest/https2/");
157 
158             proxy = DigestEchoServer.createHttpsProxyTunnel(
159                     DigestEchoServer.HttpAuthSchemeType.NONE);
160             proxyAddress = proxy.getProxyAddress();
161             proxySelector = new HttpProxySelector(proxyAddress);
162             client = newHttpClient(proxySelector, auth);
163 
164             System.out.println("Setup: done");
165         } catch (Exception x) {
166             tearDown();
167             throw x;
168         } catch (Error e) {
169             tearDown();
170             throw e;
171         }
172     }
173 
174     @AfterClass
175     public void tearDown() {
176         proxy = stop(proxy, DigestEchoServer.TunnelingProxy::stop);
177         http1Server = stop(http1Server, HttpTestServer::stop);
178         https1Server = stop(https1Server, HttpTestServer::stop);
179         http2Server = stop(http2Server, HttpTestServer::stop);
180         https2Server = stop(https2Server, HttpTestServer::stop);
181         client.close();
182         virtualExecutor.close();
183 
184         System.out.println("Teardown: done");
185     }
186 
187     private interface Stoppable<T> {
188         void stop(T service) throws Exception;
189     }
190 
191     static <T> T stop(T service, Stoppable<T> stop) {
192         try {
193             if (service != null) stop.stop(service);
194         } catch (Throwable x) {
195         }
196         return null;
197     }
198 
199     static class HttpProxySelector extends ProxySelector {
200         private static final List<Proxy> NO_PROXY = List.of(Proxy.NO_PROXY);
201         private final List<Proxy> proxyList;
202 
203         HttpProxySelector(InetSocketAddress proxyAddress) {
204             proxyList = List.of(new Proxy(Proxy.Type.HTTP, proxyAddress));
205         }
206 
207         @Override
208         public List<Proxy> select(URI uri) {
209             // Our proxy only supports tunneling
210             if (uri.getScheme().equalsIgnoreCase("https")) {
211                 if (uri.getPath().contains("/proxy/")) {
212                     return proxyList;
213                 }
214             }
215             return NO_PROXY;
216         }
217 
218         @Override
219         public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
220             System.err.println("Connection to proxy failed: " + ioe);
221             System.err.println("Proxy: " + sa);
222             System.err.println("\tURI: " + uri);
223             ioe.printStackTrace();
224         }
225     }
226 
227     public static class TestHandler implements HttpTestHandler {
228         static final AtomicLong respCounter = new AtomicLong();
229 
230         @Override
231         public void handle(HttpTestExchange t) throws IOException {
232             var count = respCounter.incrementAndGet();
233             System.out.println("Responses handled: " + count);
234             t.getRequestBody().readAllBytes();
235 
236             if (t.getRequestMethod().equalsIgnoreCase("GET")) {
237                 if (!t.getRequestHeaders().containsKey("Authorization")) {
238                     t.getResponseHeaders()
239                             .addHeader("WWW-Authenticate", "Basic realm=\"Earth\"");
240                     t.sendResponseHeaders(401, 0);
241                 } else {
242                     byte[] resp = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8);
243                     t.sendResponseHeaders(200, resp.length);
244                     try {
245                         barrier.await();
246                     } catch (Exception e) {
247                         throw new IOException(e);
248                     }
249                     t.getResponseBody().write(resp);
250                 }
251             }
252             t.close();
253         }
254     }
255 
256     void doClient(List<URI> uris) {
257         assert uris.size() == URI_COUNT;
258         barrier.reset();
259         System.out.println("Client opening connection to: " + uris.toString());
260 
261         List<CompletableFuture<HttpResponse<String>>> cfs = new ArrayList<>();
262 
263         for (int i = 0; i < REQUEST_COUNT; i++) {
264             for (URI uri : uris) {
265                 HttpRequest req = HttpRequest.newBuilder()
266                         .uri(uri)
267                         .build();
268                 cfs.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()));
269             }
270         }
271         CompletableFuture.allOf(cfs.toArray(new CompletableFuture[0])).join();
272     }
273 
274     static final class MyAuthenticator extends Authenticator {
275         private int count = 0;
276 
277         MyAuthenticator() {
278             super();
279         }
280 
281         public PasswordAuthentication getPasswordAuthentication() {
282             return (new PasswordAuthentication("user" + count,
283                     ("passwordNotCheckedAnyway" + count).toCharArray()));
284         }
285 
286         @Override
287         public PasswordAuthentication requestPasswordAuthenticationInstance(String host,
288                                                                             InetAddress addr,
289                                                                             int port,
290                                                                             String protocol,
291                                                                             String prompt,
292                                                                             String scheme,
293                                                                             URL url,
294                                                                             RequestorType reqType) {
295             PasswordAuthentication passwordAuthentication;
296             int count;
297             synchronized (this) {
298                 count = ++this.count;
299                 passwordAuthentication = super.requestPasswordAuthenticationInstance(
300                         host, addr, port, protocol, prompt, scheme, url, reqType);
301             }
302             // log outside of synchronized block
303             System.out.println("Authenticator called: " + count);
304             return passwordAuthentication;
305         }
306 
307         public int getCount() {
308             return count;
309         }
310     }
311 
312     @Test(dataProvider = "uris")
313     public void test(List<URI> uris) throws Exception {
314         System.out.println("Server listening at " + uris.toString());
315         doClient(uris);
316     }
317 }