1 /*
2  * Copyright (c) 2017, 2018, 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 /*
25  * @test
26  * @summary Test for CONTINUATION frame handling
27  * @modules java.base/sun.net.www.http
28  *          java.net.http/jdk.internal.net.http.common
29  *          java.net.http/jdk.internal.net.http.frame
30  *          java.net.http/jdk.internal.net.http.hpack
31  * @library /lib/testlibrary server
32  * @build Http2TestServer
33  * @build jdk.testlibrary.SimpleSSLContext
34  * @run testng/othervm ContinuationFrameTest
35  */
36 
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.net.URI;
41 import java.net.http.HttpHeaders;
42 import java.nio.ByteBuffer;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.function.BiFunction;
46 import javax.net.ssl.SSLContext;
47 import javax.net.ssl.SSLSession;
48 import java.net.http.HttpClient;
49 import java.net.http.HttpRequest;
50 import java.net.http.HttpRequest.BodyPublishers;
51 import java.net.http.HttpResponse;
52 import java.net.http.HttpResponse.BodyHandlers;
53 import jdk.internal.net.http.common.HttpHeadersBuilder;
54 import jdk.internal.net.http.frame.ContinuationFrame;
55 import jdk.internal.net.http.frame.HeaderFrame;
56 import jdk.internal.net.http.frame.HeadersFrame;
57 import jdk.internal.net.http.frame.Http2Frame;
58 import jdk.testlibrary.SimpleSSLContext;
59 import org.testng.annotations.AfterTest;
60 import org.testng.annotations.BeforeTest;
61 import org.testng.annotations.DataProvider;
62 import org.testng.annotations.Test;
63 import static java.lang.System.out;
64 import static java.net.http.HttpClient.Version.HTTP_2;
65 import static org.testng.Assert.assertEquals;
66 import static org.testng.Assert.assertTrue;
67 
68 public class ContinuationFrameTest {
69 
70     SSLContext sslContext;
71     Http2TestServer http2TestServer;   // HTTP/2 ( h2c )
72     Http2TestServer https2TestServer;  // HTTP/2 ( h2  )
73     String http2URI;
74     String https2URI;
75 
76     /**
77      * A function that returns a list of 1) a HEADERS frame ( with an empty
78      * payload ), and 2) a CONTINUATION frame with the actual headers.
79      */
80     static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
81         (Integer streamid, List<ByteBuffer> encodedHeaders) -> {
82             List<ByteBuffer> empty =  List.of(ByteBuffer.wrap(new byte[0]));
83             HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
84             ContinuationFrame cf = new ContinuationFrame(streamid,
85                                                          HeaderFrame.END_HEADERS,
86                                                          encodedHeaders);
87             return List.of(hf, cf);
88         };
89 
90     /**
91      * A function that returns a list of a HEADERS frame followed by a number of
92      * CONTINUATION frames. Each frame contains just a single byte of payload.
93      */
94     static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
95         (Integer streamid, List<ByteBuffer> encodedHeaders) -> {
96             assert encodedHeaders.get(0).hasRemaining();
97             List<Http2Frame> frames = new ArrayList<>();
98             ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
99             HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
100             frames.add(hf);
101             for (ByteBuffer bb : encodedHeaders) {
102                 while (bb.hasRemaining()) {
103                     List<ByteBuffer> data = List.of(ByteBuffer.wrap(new byte[] {bb.get()}));
104                     ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
105                     frames.add(cf);
106                 }
107             }
108             frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
109             return frames;
110         };
111 
112     @DataProvider(name = "variants")
variants()113     public Object[][] variants() {
114         return new Object[][] {
115                 { http2URI,  false, oneContinuation },
116                 { https2URI, false, oneContinuation },
117                 { http2URI,  true,  oneContinuation },
118                 { https2URI, true,  oneContinuation },
119 
120                 { http2URI,  false, byteAtATime },
121                 { https2URI, false, byteAtATime },
122                 { http2URI,  true,  byteAtATime },
123                 { https2URI, true,  byteAtATime },
124         };
125     }
126 
127     static final int ITERATION_COUNT = 20;
128 
129     @Test(dataProvider = "variants")
test(String uri, boolean sameClient, BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)130     void test(String uri,
131               boolean sameClient,
132               BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
133         throws Exception
134     {
135         CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
136 
137         HttpClient client = null;
138         for (int i=0; i< ITERATION_COUNT; i++) {
139             if (!sameClient || client == null)
140                 client = HttpClient.newBuilder().sslContext(sslContext).build();
141 
142             HttpRequest request = HttpRequest.newBuilder(URI.create(uri))
143                                              .POST(BodyPublishers.ofString("Hello there!"))
144                                              .build();
145             HttpResponse<String> resp;
146             if (i % 2 == 0) {
147                 resp = client.send(request, BodyHandlers.ofString());
148             } else {
149                 resp = client.sendAsync(request, BodyHandlers.ofString()).join();
150             }
151 
152             out.println("Got response: " + resp);
153             out.println("Got body: " + resp.body());
154             assertTrue(resp.statusCode() == 200,
155                        "Expected 200, got:" + resp.statusCode());
156             assertEquals(resp.body(), "Hello there!");
157             assertEquals(resp.version(), HTTP_2);
158         }
159     }
160 
161     @BeforeTest
setup()162     public void setup() throws Exception {
163         sslContext = new SimpleSSLContext().get();
164         if (sslContext == null)
165             throw new AssertionError("Unexpected null sslContext");
166 
167         http2TestServer = new Http2TestServer("localhost", false, 0);
168         http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
169         int port = http2TestServer.getAddress().getPort();
170         http2URI = "http://localhost:" + port + "/http2/echo";
171 
172         https2TestServer = new Http2TestServer("localhost", true, sslContext);
173         https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
174         port = https2TestServer.getAddress().getPort();
175         https2URI = "https://localhost:" + port + "/https2/echo";
176 
177         // Override the default exchange supplier with a custom one to enable
178         // particular test scenarios
179         http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
180         https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
181 
182         http2TestServer.start();
183         https2TestServer.start();
184     }
185 
186     @AfterTest
teardown()187     public void teardown() throws Exception {
188         http2TestServer.stop();
189         https2TestServer.stop();
190     }
191 
192     static class Http2EchoHandler implements Http2Handler {
193         @Override
handle(Http2TestExchange t)194         public void handle(Http2TestExchange t) throws IOException {
195             try (InputStream is = t.getRequestBody();
196                  OutputStream os = t.getResponseBody()) {
197                 byte[] bytes = is.readAllBytes();
198                 t.getResponseHeaders().addHeader("justSome", "Noise");
199                 t.getResponseHeaders().addHeader("toAdd", "payload in");
200                 t.getResponseHeaders().addHeader("theHeader", "Frames");
201                 t.sendResponseHeaders(200, bytes.length);
202                 os.write(bytes);
203             }
204         }
205     }
206 
207     // A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
208     // allow headers to be sent with a number of CONTINUATION frames.
209     static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
210         static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
211 
setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs)212         static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
213             headerFrameSupplier = hfs;
214         }
215 
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed)216         CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
217                              HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
218                              SSLSession sslSession, BodyOutputStream os,
219                              Http2TestServerConnection conn, boolean pushAllowed) {
220             super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
221                   os, conn, pushAllowed);
222 
223         }
224 
225         @Override
sendResponseHeaders(int rCode, long responseLength)226         public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
227             this.responseLength = responseLength;
228             if (responseLength > 0 || responseLength < 0) {
229                 long clen = responseLength > 0 ? responseLength : 0;
230                 rspheadersBuilder.setHeader("Content-length", Long.toString(clen));
231             }
232             rspheadersBuilder.setHeader(":status", Integer.toString(rCode));
233             HttpHeaders headers = rspheadersBuilder.build();
234 
235             List<ByteBuffer> encodeHeaders = conn.encodeHeaders(headers);
236             List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
237             assert headerFrames.size() > 0;  // there must always be at least 1
238 
239             if (responseLength < 0) {
240                 headerFrames.get(headerFrames.size() -1).setFlag(HeadersFrame.END_STREAM);
241                 os.closeInternal();
242             }
243 
244             for (Http2Frame f : headerFrames)
245                 conn.outputQ.put(f);
246 
247             os.goodToGo();
248             System.err.println("Sent response headers " + rCode);
249         }
250     }
251 }
252