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