/* * Copyright (c) 2017, 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 Test for CONTINUATION frame handling * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext * @compile ../ReferenceTracker.java * @run testng/othervm ContinuationFrameTest */ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.http.HttpHeaders; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import jdk.internal.net.http.common.HttpHeadersBuilder; import jdk.internal.net.http.frame.ContinuationFrame; import jdk.internal.net.http.frame.HeaderFrame; import jdk.internal.net.http.frame.HeadersFrame; import jdk.internal.net.http.frame.Http2Frame; import jdk.httpclient.test.lib.http2.Http2TestServer; import jdk.httpclient.test.lib.http2.Http2TestExchange; import jdk.httpclient.test.lib.http2.Http2TestExchangeImpl; import jdk.httpclient.test.lib.http2.Http2Handler; import jdk.httpclient.test.lib.http2.BodyOutputStream; import jdk.httpclient.test.lib.http2.Http2TestServerConnection; import jdk.test.lib.net.SimpleSSLContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_2; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; public class ContinuationFrameTest { SSLContext sslContext; Http2TestServer http2TestServer; // HTTP/2 ( h2c ) Http2TestServer https2TestServer; // HTTP/2 ( h2 ) String http2URI; String https2URI; String noBodyhttp2URI; String noBodyhttps2URI; final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; /** * A function that returns a list of 1) a HEADERS frame ( with an empty * payload ), and 2) a CONTINUATION frame with the actual headers. */ static BiFunction,List> oneContinuation = (Integer streamid, List encodedHeaders) -> { List empty = List.of(ByteBuffer.wrap(new byte[0])); HeadersFrame hf = new HeadersFrame(streamid, 0, empty); ContinuationFrame cf = new ContinuationFrame(streamid, HeaderFrame.END_HEADERS, encodedHeaders); return List.of(hf, cf); }; /** * A function that returns a list of 1) a HEADERS frame with END_STREAM * ( and with an empty payload ), and 2) two CONTINUATION frames,the first * is empty and the second contains headers and the END_HEADERS flag */ static BiFunction,List> twoContinuation = (Integer streamid, List encodedHeaders) -> { List empty = List.of(ByteBuffer.wrap(new byte[0])); HeadersFrame hf = new HeadersFrame(streamid, HeaderFrame.END_STREAM, empty); ContinuationFrame cf = new ContinuationFrame(streamid, 0,empty); ContinuationFrame cf1 = new ContinuationFrame(streamid, HeaderFrame.END_HEADERS, encodedHeaders); return List.of(hf, cf, cf1); }; /** * A function that returns a list of a HEADERS frame followed by a number of * CONTINUATION frames. Each frame contains just a single byte of payload. */ static BiFunction,List> byteAtATime = (Integer streamid, List encodedHeaders) -> { assert encodedHeaders.get(0).hasRemaining(); List frames = new ArrayList<>(); ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()}); HeadersFrame hf = new HeadersFrame(streamid, 0, hb); frames.add(hf); for (ByteBuffer bb : encodedHeaders) { while (bb.hasRemaining()) { List data = List.of(ByteBuffer.wrap(new byte[] {bb.get()})); ContinuationFrame cf = new ContinuationFrame(streamid, 0, data); frames.add(cf); } } frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS); return frames; }; @DataProvider(name = "variants") public Object[][] variants() { return new Object[][] { { http2URI, false, oneContinuation }, { https2URI, false, oneContinuation }, { http2URI, true, oneContinuation }, { https2URI, true, oneContinuation }, { noBodyhttp2URI, false, twoContinuation }, { noBodyhttp2URI, true, twoContinuation }, { noBodyhttps2URI, false, twoContinuation }, { noBodyhttps2URI, true, twoContinuation }, { http2URI, false, byteAtATime }, { https2URI, false, byteAtATime }, { http2URI, true, byteAtATime }, { https2URI, true, byteAtATime }, }; } static final int ITERATION_COUNT = 20; @Test(dataProvider = "variants") void test(String uri, boolean sameClient, BiFunction,List> headerFramesSupplier) throws Exception { CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier); HttpClient client = null; for (int i=0; i< ITERATION_COUNT; i++) { if (!sameClient || client == null) { client = HttpClient.newBuilder() .proxy(HttpClient.Builder.NO_PROXY) .sslContext(sslContext) .build(); TRACKER.track(client); } HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) .POST(BodyPublishers.ofString("Hello there!")) .build(); HttpResponse resp; if (i % 2 == 0) { resp = client.send(request, BodyHandlers.ofString()); } else { resp = client.sendAsync(request, BodyHandlers.ofString()).join(); } if(uri.contains("nobody")) { out.println("Got response: " + resp); assertTrue(resp.statusCode() == 204, "Expected 204, got:" + resp.statusCode()); assertEquals(resp.version(), HTTP_2); continue; } out.println("Got response: " + resp); out.println("Got body: " + resp.body()); assertTrue(resp.statusCode() == 200, "Expected 200, got:" + resp.statusCode()); assertEquals(resp.body(), "Hello there!"); assertEquals(resp.version(), HTTP_2); } } @BeforeTest public void setup() throws Exception { sslContext = new SimpleSSLContext().get(); if (sslContext == null) throw new AssertionError("Unexpected null sslContext"); http2TestServer = new Http2TestServer("localhost", false, 0); http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo"); http2TestServer.addHandler(new Http2NoBodyHandler(), "/http2/nobody"); int port = http2TestServer.getAddress().getPort(); http2URI = "http://localhost:" + port + "/http2/echo"; noBodyhttp2URI = "http://localhost:" + port + "/http2/nobody"; https2TestServer = new Http2TestServer("localhost", true, sslContext); https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo"); https2TestServer.addHandler(new Http2NoBodyHandler(), "/https2/nobody"); port = https2TestServer.getAddress().getPort(); https2URI = "https://localhost:" + port + "/https2/echo"; noBodyhttps2URI = "https://localhost:" + port + "/https2/nobody"; // Override the default exchange supplier with a custom one to enable // particular test scenarios http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new); https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new); http2TestServer.start(); https2TestServer.start(); } @AfterTest public void teardown() throws Exception { AssertionError fail = TRACKER.check(500); try { http2TestServer.stop(); https2TestServer.stop(); } finally { if (fail != null) { throw fail; } } } static class Http2EchoHandler implements Http2Handler { @Override public void handle(Http2TestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); t.getResponseHeaders().addHeader("justSome", "Noise"); t.getResponseHeaders().addHeader("toAdd", "payload in"); t.getResponseHeaders().addHeader("theHeader", "Frames"); t.sendResponseHeaders(200, bytes.length); os.write(bytes); } } } static class Http2NoBodyHandler implements Http2Handler { @Override public void handle(Http2TestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); t.sendResponseHeaders(204, -1); } } } // A custom Http2TestExchangeImpl that overrides sendResponseHeaders to // allow headers to be sent with a number of CONTINUATION frames. static class CFTHttp2TestExchange extends Http2TestExchangeImpl { static volatile BiFunction,List> headerFrameSupplier; static void setHeaderFrameSupplier(BiFunction,List> hfs) { headerFrameSupplier = hfs; } CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) { super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed); } @Override public void sendResponseHeaders(int rCode, long responseLength) throws IOException { this.responseLength = responseLength; if (responseLength != 0 && rCode != 204) { long clen = responseLength > 0 ? responseLength : 0; rspheadersBuilder.setHeader("Content-length", Long.toString(clen)); } rspheadersBuilder.setHeader(":status", Integer.toString(rCode)); HttpHeaders headers = rspheadersBuilder.build(); List encodeHeaders = conn.encodeHeaders(headers); List headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders); assert headerFrames.size() > 0; // there must always be at least 1 if(headerFrames.get(0).getFlag(HeaderFrame.END_STREAM)) os.markClosed(); for (Http2Frame f : headerFrames) { conn.addToOutputQ(f); } os.goodToGo(); System.err.println("Sent response headers " + rCode); } } }