/* * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * @test * @summary Tests an asynchronous BodySubscriber that completes * immediately with an InputStream which issues bad * requests * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.test.lib.net.SimpleSSLContext ReferenceTracker * jdk.httpclient.test.lib.common.HttpServerAdapters * @run testng/othervm InvalidInputStreamSubscriptionRequest */ import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpsConfigurator; import com.sun.net.httpserver.HttpsServer; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandler; import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodySubscriber; import java.net.http.HttpResponse.BodySubscribers; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; import static java.nio.charset.StandardCharsets.UTF_8; import static org.testng.Assert.assertEquals; public class InvalidInputStreamSubscriptionRequest implements HttpServerAdapters { SSLContext sslContext; HttpTestServer httpTestServer; // HTTP/1.1 [ 4 servers ] HttpTestServer httpsTestServer; // HTTPS/1.1 HttpTestServer http2TestServer; // HTTP/2 ( h2c ) HttpTestServer https2TestServer; // HTTP/2 ( h2 ) String httpURI_fixed; String httpURI_chunk; String httpsURI_fixed; String httpsURI_chunk; String http2URI_fixed; String http2URI_chunk; String https2URI_fixed; String https2URI_chunk; static final int ITERATION_COUNT = 3; // a shared executor helps reduce the amount of threads created by the test static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); static final long start = System.nanoTime(); public static String now() { long now = System.nanoTime() - start; long secs = now / 1000_000_000; long mill = (now % 1000_000_000) / 1000_000; long nan = now % 1000_000; return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); } static volatile boolean tasksFailed; static final ConcurrentMap FAILURES = new ConcurrentHashMap<>(); static class TestExecutor implements Executor { final AtomicLong tasks = new AtomicLong(); Executor executor; TestExecutor(Executor executor) { this.executor = executor; } @Override public void execute(Runnable command) { long id = tasks.incrementAndGet(); executor.execute(() -> { try { command.run(); } catch (Throwable t) { tasksFailed = true; System.out.printf(now() + "Task %s failed: %s%n", id, t); System.err.printf(now() + "Task %s failed: %s%n", id, t); FAILURES.putIfAbsent("Task " + id, t); throw t; } }); } } @AfterClass static final void printFailedTests() { out.println("\n========================="); try { out.println("Failed tasks: "); FAILURES.entrySet().forEach((e) -> { out.printf("\t%s: %s%n", e.getKey(), e.getValue()); e.getValue().printStackTrace(out); }); if (tasksFailed) { System.out.println("WARNING: Some tasks failed"); } } finally { out.println("\n=========================\n"); } } interface BHS extends Supplier> { static BHS of(BHS impl, String name) { return new BHSImpl(impl, name); } } static final class BHSImpl implements BHS { final BHS supplier; final String name; BHSImpl(BHS impl, String name) { this.supplier = impl; this.name = name; } @Override public String toString() { return name; } @Override public BodyHandler get() { return supplier.get(); } } static final Supplier> OF_INPUTSTREAM = BHS.of(BodyHandlers::ofInputStream, "BodyHandlers::ofInputStream"); @DataProvider(name = "variants") public Object[][] variants() { return new Object[][]{ { httpURI_fixed, false, OF_INPUTSTREAM }, { httpURI_chunk, false, OF_INPUTSTREAM }, { httpsURI_fixed, false, OF_INPUTSTREAM }, { httpsURI_chunk, false, OF_INPUTSTREAM }, { http2URI_fixed, false, OF_INPUTSTREAM }, { http2URI_chunk, false, OF_INPUTSTREAM }, { https2URI_fixed, false, OF_INPUTSTREAM }, { https2URI_chunk, false, OF_INPUTSTREAM }, { httpURI_fixed, true, OF_INPUTSTREAM }, { httpURI_chunk, true, OF_INPUTSTREAM }, { httpsURI_fixed, true, OF_INPUTSTREAM }, { httpsURI_chunk, true, OF_INPUTSTREAM }, { http2URI_fixed, true, OF_INPUTSTREAM }, { http2URI_chunk, true, OF_INPUTSTREAM }, { https2URI_fixed, true, OF_INPUTSTREAM }, { https2URI_chunk, true, OF_INPUTSTREAM }, }; } final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; HttpClient newHttpClient() { return TRACKER.track(HttpClient.newBuilder() .proxy(HttpClient.Builder.NO_PROXY) .executor(executor) .sslContext(sslContext) .build()); } @Test(dataProvider = "variants") public void testNoBody(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) client = newHttpClient(); HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> new BadBodySubscriber<>(handler.apply(rspinfo)); try { HttpResponse response = client.send(req, badHandler); try (InputStream is = response.body()) { String body = new String(is.readAllBytes(), UTF_8); assertEquals(body, ""); if (uri.endsWith("/chunk") && response.version() == HTTP_1_1) { // with /fixed and 0 length // there's no need for any call to request() throw new RuntimeException("Expected IAE not thrown"); } } } catch (Exception x) { Throwable cause = x; if (x instanceof CompletionException || x instanceof ExecutionException) { cause = x.getCause(); } if (cause instanceof IOException && cause.getCause() != null) { cause = cause.getCause(); } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); } else throw x; } } } @Test(dataProvider = "variants") public void testNoBodyAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) client = newHttpClient(); HttpRequest req = HttpRequest.newBuilder(URI.create(uri)) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> new BadBodySubscriber<>(handler.apply(rspinfo)); CompletableFuture> response = client.sendAsync(req, badHandler); CompletableFuture result = response.thenCompose( (responsePublisher) -> { try (InputStream is = responsePublisher.body()) { return CompletableFuture.completedFuture( new String(is.readAllBytes(), UTF_8)); } catch (Exception x) { return CompletableFuture.failedFuture(x); } }); try { // Get the final result and compare it with the expected body assertEquals(result.get(), ""); if (uri.endsWith("/chunk") && response.get().version() == HTTP_1_1) { // with /fixed and 0 length // there's no need for any call to request() throw new RuntimeException("Expected IAE not thrown"); } } catch (Exception x) { Throwable cause = x; if (x instanceof CompletionException || x instanceof ExecutionException) { cause = x.getCause(); } if (cause instanceof IOException && cause.getCause() != null) { cause = cause.getCause(); } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); } else throw x; } } } @Test(dataProvider = "variants") public void testAsString(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) client = newHttpClient(); HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> new BadBodySubscriber<>(handler.apply(rspinfo)); try { HttpResponse response = client.send(req, badHandler); try (InputStream is = response.body()) { String body = new String(is.readAllBytes(), UTF_8); assertEquals(body, WITH_BODY); throw new RuntimeException("Expected IAE not thrown"); } } catch (Exception x) { Throwable cause = x; if (x instanceof CompletionException || x instanceof ExecutionException) { cause = x.getCause(); } if (cause instanceof IOException && cause.getCause() != null) { cause = cause.getCause(); } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); } else throw x; } } } @Test(dataProvider = "variants") public void testAsStringAsync(String uri, boolean sameClient, BHS handlers) throws Exception { HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) client = newHttpClient(); HttpRequest req = HttpRequest.newBuilder(URI.create(uri+"/withBody")) .build(); BodyHandler handler = handlers.get(); BodyHandler badHandler = (rspinfo) -> new BadBodySubscriber<>(handler.apply(rspinfo)); CompletableFuture result = client.sendAsync(req, badHandler) .thenCompose((responsePublisher) -> { try (InputStream is = responsePublisher.body()) { return CompletableFuture.completedFuture( new String(is.readAllBytes(), UTF_8)); } catch (Exception x) { return CompletableFuture.failedFuture(x); } }); // Get the final result and compare it with the expected body try { String body = result.get(); assertEquals(body, WITH_BODY); throw new RuntimeException("Expected IAE not thrown"); } catch (Exception x) { Throwable cause = x; if (x instanceof CompletionException || x instanceof ExecutionException) { cause = x.getCause(); } if (cause instanceof IOException && cause.getCause() != null) { cause = cause.getCause(); } if (cause instanceof IllegalArgumentException) { System.out.println("Got expected exception: " + cause); } else throw x; } } } static final class BadSubscription implements Flow.Subscription { Flow.Subscription subscription; Executor executor; BadSubscription(Flow.Subscription subscription) { this.subscription = subscription; } @Override public void request(long n) { if (executor == null) { subscription.request(-n); } else { executor.execute(() -> subscription.request(-n)); } } @Override public void cancel() { subscription.cancel(); } } static final class BadBodySubscriber implements BodySubscriber { final BodySubscriber subscriber; BadBodySubscriber(BodySubscriber subscriber) { this.subscriber = subscriber; } @Override public CompletionStage getBody() { return subscriber.getBody(); } @Override public void onSubscribe(Flow.Subscription subscription) { System.out.println("Subscription is: " + subscription); subscriber.onSubscribe(new BadSubscription(subscription)); } @Override public void onNext(List item) { subscriber.onNext(item); } @Override public void onError(Throwable throwable) { subscriber.onError(throwable); } @Override public void onComplete() { subscriber.onComplete(); } } static String serverAuthority(HttpServer server) { return InetAddress.getLoopbackAddress().getHostName() + ":" + server.getAddress().getPort(); } @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); // HTTP/1.1 HttpTestHandler h1_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h1_chunkHandler = new HTTP_VariableLengthHandler(); httpTestServer = HttpTestServer.create(HTTP_1_1); httpTestServer.addHandler( h1_fixedLengthHandler, "/http1/fixed"); httpTestServer.addHandler(h1_chunkHandler,"/http1/chunk"); httpURI_fixed = "http://" + httpTestServer.serverAuthority() + "/http1/fixed"; httpURI_chunk = "http://" + httpTestServer.serverAuthority() + "/http1/chunk"; httpsTestServer = HttpTestServer.create(HTTP_1_1, sslContext); httpsTestServer.addHandler(h1_fixedLengthHandler, "/https1/fixed"); httpsTestServer.addHandler(h1_chunkHandler, "/https1/chunk"); httpsURI_fixed = "https://" + httpsTestServer.serverAuthority() + "/https1/fixed"; httpsURI_chunk = "https://" + httpsTestServer.serverAuthority() + "/https1/chunk"; // HTTP/2 HttpTestHandler h2_fixedLengthHandler = new HTTP_FixedLengthHandler(); HttpTestHandler h2_chunkedHandler = new HTTP_VariableLengthHandler(); http2TestServer = HttpTestServer.create(HTTP_2); http2TestServer.addHandler(h2_fixedLengthHandler, "/http2/fixed"); http2TestServer.addHandler(h2_chunkedHandler, "/http2/chunk"); http2URI_fixed = "http://" + http2TestServer.serverAuthority() + "/http2/fixed"; http2URI_chunk = "http://" + http2TestServer.serverAuthority() + "/http2/chunk"; https2TestServer = HttpTestServer.create(HTTP_2, sslContext); https2TestServer.addHandler(h2_fixedLengthHandler, "/https2/fixed"); https2TestServer.addHandler(h2_chunkedHandler, "/https2/chunk"); https2URI_fixed = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; https2URI_chunk = "https://" + https2TestServer.serverAuthority() + "/https2/chunk"; httpTestServer.start(); httpsTestServer.start(); http2TestServer.start(); https2TestServer.start(); } @AfterTest public void teardown() throws Exception { AssertionError fail = TRACKER.check(500); try { httpTestServer.stop(); httpsTestServer.stop(); http2TestServer.stop(); https2TestServer.stop(); } finally { if (fail != null) { throw fail; } } } static final String WITH_BODY = "Lorem ipsum dolor sit amet, consectetur" + " adipiscing elit, sed do eiusmod tempor incididunt ut labore et" + " dolore magna aliqua. Ut enim ad minim veniam, quis nostrud" + " exercitation ullamco laboris nisi ut aliquip ex ea" + " commodo consequat. Duis aute irure dolor in reprehenderit in " + "voluptate velit esse cillum dolore eu fugiat nulla pariatur." + " Excepteur sint occaecat cupidatat non proident, sunt in culpa qui" + " officia deserunt mollit anim id est laborum."; static class HTTP_FixedLengthHandler implements HttpTestHandler { @Override public void handle(HttpTestExchange t) throws IOException { out.println("HTTP_FixedLengthHandler received request to " + t.getRequestURI()); try (InputStream is = t.getRequestBody()) { is.readAllBytes(); } if (t.getRequestURI().getPath().endsWith("/withBody")) { byte[] bytes = WITH_BODY.getBytes(UTF_8); t.sendResponseHeaders(200, bytes.length); // body try (OutputStream os = t.getResponseBody()) { os.write(bytes); } } else { t.sendResponseHeaders(200, 0); //no body } } } static class HTTP_VariableLengthHandler implements HttpTestHandler { @Override public void handle(HttpTestExchange t) throws IOException { out.println("HTTP_VariableLengthHandler received request to " + t.getRequestURI()); try (InputStream is = t.getRequestBody()) { is.readAllBytes(); } t.sendResponseHeaders(200, -1); //chunked or variable if (t.getRequestURI().getPath().endsWith("/withBody")) { byte[] bytes = WITH_BODY.getBytes(UTF_8); try (OutputStream os = t.getResponseBody()) { int chunkLen = bytes.length/10; if (chunkLen == 0) { os.write(bytes); } else { int count = 0; for (int i=0; i<10; i++) { os.write(bytes, count, chunkLen); os.flush(); count += chunkLen; } os.write(bytes, count, bytes.length % chunkLen); count += bytes.length % chunkLen; assert count == bytes.length; } } } else { t.getResponseBody().close(); // no body } } } }