1 /*
2  * Copyright (c) 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  * @modules java.base/sun.net.www.http
27  *          java.net.http/jdk.internal.net.http.common
28  *          java.net.http/jdk.internal.net.http.frame
29  *          java.net.http/jdk.internal.net.http.hpack
30  * @library /test/lib server
31  * @build Http2TestServer
32  * @build jdk.test.lib.net.SimpleSSLContext
33  * @run testng/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
34  */
35 
36 import jdk.internal.net.http.common.HttpHeadersBuilder;
37 import jdk.internal.net.http.frame.ContinuationFrame;
38 import jdk.internal.net.http.frame.HeaderFrame;
39 import jdk.internal.net.http.frame.HeadersFrame;
40 import jdk.internal.net.http.frame.Http2Frame;
41 import jdk.test.lib.net.SimpleSSLContext;
42 import org.testng.annotations.AfterTest;
43 import org.testng.annotations.BeforeTest;
44 import org.testng.annotations.DataProvider;
45 import org.testng.annotations.Test;
46 import javax.net.ssl.SSLContext;
47 import javax.net.ssl.SSLSession;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.OutputStream;
51 import java.net.URI;
52 import java.net.http.HttpClient;
53 import java.net.http.HttpHeaders;
54 import java.net.http.HttpRequest;
55 import java.net.http.HttpRequest.BodyPublishers;
56 import java.net.http.HttpResponse;
57 import java.net.http.HttpResponse.BodyHandlers;
58 import java.nio.ByteBuffer;
59 import java.util.ArrayList;
60 import java.util.List;
61 import java.util.Map.Entry;
62 import java.util.concurrent.ExecutionException;
63 import java.util.function.BiFunction;
64 import static java.util.List.of;
65 import static java.util.Map.entry;
66 import static org.testng.Assert.assertTrue;
67 import static org.testng.Assert.fail;
68 
69 // Code copied from ContinuationFrameTest
70 public class BadHeadersTest {
71 
72     private static final List<List<Entry<String, String>>> BAD_HEADERS = of(
73         of(entry(":status", "200"),  entry(":hello", "GET")),                      // Unknown pseudo-header
74         of(entry(":status", "200"),  entry("hell o", "value")),                    // Space in the name
75         of(entry(":status", "200"),  entry("hello", "line1\r\n  line2\r\n")),      // Multiline value
76         of(entry(":status", "200"),  entry("hello", "DE" + ((char) 0x7F) + "L")),  // Bad byte in value
77         of(entry("hello", "world!"), entry(":status", "200"))                      // Pseudo header is not the first one
78     );
79 
80     SSLContext sslContext;
81     Http2TestServer http2TestServer;   // HTTP/2 ( h2c )
82     Http2TestServer https2TestServer;  // HTTP/2 ( h2  )
83     String http2URI;
84     String https2URI;
85 
86     /**
87      * A function that returns a list of 1) a HEADERS frame ( with an empty
88      * payload ), and 2) a CONTINUATION frame with the actual headers.
89      */
90     static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
91             (Integer streamid, List<ByteBuffer> encodedHeaders) -> {
92                 List<ByteBuffer> empty =  of(ByteBuffer.wrap(new byte[0]));
93                 HeadersFrame hf = new HeadersFrame(streamid, 0, empty);
94                 ContinuationFrame cf = new ContinuationFrame(streamid,
95                                                              HeaderFrame.END_HEADERS,
96                                                              encodedHeaders);
97                 return of(hf, cf);
98             };
99 
100     /**
101      * A function that returns a list of a HEADERS frame followed by a number of
102      * CONTINUATION frames. Each frame contains just a single byte of payload.
103      */
104     static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
105             (Integer streamid, List<ByteBuffer> encodedHeaders) -> {
106                 assert encodedHeaders.get(0).hasRemaining();
107                 List<Http2Frame> frames = new ArrayList<>();
108                 ByteBuffer hb = ByteBuffer.wrap(new byte[] {encodedHeaders.get(0).get()});
109                 HeadersFrame hf = new HeadersFrame(streamid, 0, hb);
110                 frames.add(hf);
111                 for (ByteBuffer bb : encodedHeaders) {
112                     while (bb.hasRemaining()) {
113                         List<ByteBuffer> data = of(ByteBuffer.wrap(new byte[] {bb.get()}));
114                         ContinuationFrame cf = new ContinuationFrame(streamid, 0, data);
115                         frames.add(cf);
116                     }
117                 }
118                 frames.get(frames.size() - 1).setFlag(HeaderFrame.END_HEADERS);
119                 return frames;
120             };
121 
122     @DataProvider(name = "variants")
variants()123     public Object[][] variants() {
124         return new Object[][] {
125                 { http2URI,  false, oneContinuation },
126                 { https2URI, false, oneContinuation },
127                 { http2URI,  true,  oneContinuation },
128                 { https2URI, true,  oneContinuation },
129 
130                 { http2URI,  false, byteAtATime },
131                 { https2URI, false, byteAtATime },
132                 { http2URI,  true,  byteAtATime },
133                 { https2URI, true,  byteAtATime },
134         };
135     }
136 
137 
138     @Test(dataProvider = "variants")
test(String uri, boolean sameClient, BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)139     void test(String uri,
140               boolean sameClient,
141               BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
142         throws Exception
143     {
144         CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
145 
146         HttpClient client = null;
147         for (int i=0; i< BAD_HEADERS.size(); i++) {
148             if (!sameClient || client == null)
149                 client = HttpClient.newBuilder().sslContext(sslContext).build();
150 
151             URI uriWithQuery = URI.create(uri +  "?BAD_HEADERS=" + i);
152             HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
153                     .POST(BodyPublishers.ofString("Hello there!"))
154                     .build();
155             System.out.println("\nSending request:" + uriWithQuery);
156             final HttpClient cc = client;
157             try {
158                 HttpResponse<String> response = cc.send(request, BodyHandlers.ofString());
159                 fail("Expected exception, got :" + response + ", " + response.body());
160             } catch (IOException ioe) {
161                 System.out.println("Got EXPECTED: " + ioe);
162                 assertDetailMessage(ioe, i);
163             }
164         }
165     }
166 
167     @Test(dataProvider = "variants")
testAsync(String uri, boolean sameClient, BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)168     void testAsync(String uri,
169                    boolean sameClient,
170                    BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFramesSupplier)
171     {
172         CFTHttp2TestExchange.setHeaderFrameSupplier(headerFramesSupplier);
173 
174         HttpClient client = null;
175         for (int i=0; i< BAD_HEADERS.size(); i++) {
176             if (!sameClient || client == null)
177                 client = HttpClient.newBuilder().sslContext(sslContext).build();
178 
179             URI uriWithQuery = URI.create(uri +  "?BAD_HEADERS=" + i);
180             HttpRequest request = HttpRequest.newBuilder(uriWithQuery)
181                     .POST(BodyPublishers.ofString("Hello there!"))
182                     .build();
183             System.out.println("\nSending request:" + uriWithQuery);
184             final HttpClient cc = client;
185 
186             Throwable t = null;
187             try {
188                 HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
189                 fail("Expected exception, got :" + response + ", " + response.body());
190             } catch (Throwable t0) {
191                 System.out.println("Got EXPECTED: " + t0);
192                 if (t0 instanceof ExecutionException) {
193                     t0 = t0.getCause();
194                 }
195                 t = t0;
196             }
197             assertDetailMessage(t, i);
198         }
199     }
200 
201     // Assertions based on implementation specific detail messages. Keep in
202     // sync with implementation.
assertDetailMessage(Throwable throwable, int iterationIndex)203     static void assertDetailMessage(Throwable throwable, int iterationIndex) {
204         assertTrue(throwable instanceof IOException,
205                    "Expected IOException, got, " + throwable);
206         assertTrue(throwable.getMessage().contains("protocol error"),
207                 "Expected \"protocol error\" in: " + throwable.getMessage());
208 
209         if (iterationIndex == 0) { // unknown
210             assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
211                     "Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
212         } else if (iterationIndex == 4) { // unexpected
213             assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
214                     "Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
215         } else {
216             assertTrue(throwable.getMessage().contains("Bad header"),
217                     "Expected \"Bad header\" in: " + throwable.getMessage());
218         }
219     }
220 
221     @BeforeTest
setup()222     public void setup() throws Exception {
223         sslContext = new SimpleSSLContext().get();
224         if (sslContext == null)
225             throw new AssertionError("Unexpected null sslContext");
226 
227         http2TestServer = new Http2TestServer("localhost", false, 0);
228         http2TestServer.addHandler(new Http2EchoHandler(), "/http2/echo");
229         int port = http2TestServer.getAddress().getPort();
230         http2URI = "http://localhost:" + port + "/http2/echo";
231 
232         https2TestServer = new Http2TestServer("localhost", true, sslContext);
233         https2TestServer.addHandler(new Http2EchoHandler(), "/https2/echo");
234         port = https2TestServer.getAddress().getPort();
235         https2URI = "https://localhost:" + port + "/https2/echo";
236 
237         // Override the default exchange supplier with a custom one to enable
238         // particular test scenarios
239         http2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
240         https2TestServer.setExchangeSupplier(CFTHttp2TestExchange::new);
241 
242         http2TestServer.start();
243         https2TestServer.start();
244     }
245 
246     @AfterTest
teardown()247     public void teardown() throws Exception {
248         http2TestServer.stop();
249         https2TestServer.stop();
250     }
251 
252     static class Http2EchoHandler implements Http2Handler {
253 
254         @Override
handle(Http2TestExchange t)255         public void handle(Http2TestExchange t) throws IOException {
256             try (InputStream is = t.getRequestBody();
257                  OutputStream os = t.getResponseBody()) {
258                 byte[] bytes = is.readAllBytes();
259                 // Note: strictly ordered response headers will be added within
260                 // the custom sendResponseHeaders implementation, based upon the
261                 // query parameter
262                 t.sendResponseHeaders(200, bytes.length);
263                 os.write(bytes);
264             }
265         }
266     }
267 
268     // A custom Http2TestExchangeImpl that overrides sendResponseHeaders to
269     // allow headers to be sent with a number of CONTINUATION frames.
270     static class CFTHttp2TestExchange extends Http2TestExchangeImpl {
271         static volatile BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> headerFrameSupplier;
272         volatile int badHeadersIndex = -1;
273 
setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs)274         static void setHeaderFrameSupplier(BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> hfs) {
275             headerFrameSupplier = hfs;
276         }
277 
CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed)278         CFTHttp2TestExchange(int streamid, String method, HttpHeaders reqheaders,
279                              HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
280                              SSLSession sslSession, BodyOutputStream os,
281                              Http2TestServerConnection conn, boolean pushAllowed) {
282             super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession,
283                   os, conn, pushAllowed);
284             String query = uri.getQuery();
285             badHeadersIndex = Integer.parseInt(query.substring(query.indexOf("=") + 1));
286             assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
287                     "Unexpected badHeadersIndex value: " + badHeadersIndex;
288         }
289 
290         @Override
291         public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
292             assert rspheadersBuilder.build().map().size() == 0;
293             assert badHeadersIndex >= 0 && badHeadersIndex < BAD_HEADERS.size() :
294                     "Unexpected badHeadersIndex value: " + badHeadersIndex;
295 
296             List<Entry<String,String>> headers = BAD_HEADERS.get(badHeadersIndex);
297             System.out.println("Server replying with bad headers: " + headers);
298             List<ByteBuffer> encodeHeaders = conn.encodeHeadersOrdered(headers);
299 
300             List<Http2Frame> headerFrames = headerFrameSupplier.apply(streamid, encodeHeaders);
301             assert headerFrames.size() > 0;  // there must always be at least 1
302 
303             if (responseLength < 0) {
304                 headerFrames.get(headerFrames.size() -1).setFlag(HeadersFrame.END_STREAM);
305                 os.closeInternal();
306             }
307 
308             for (Http2Frame f : headerFrames)
309                 conn.outputQ.put(f);
310 
311             os.goodToGo();
312             System.err.println("Sent response headers " + rCode);
313         }
314     }
315 }
316