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