1 /*
2  * Copyright (c) 2019, 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  * @bug 8185898
27  * @modules java.base/sun.net.www
28  * @library /test/lib
29  * @run main/othervm B8185898
30  * @summary setRequestProperty(key, null) results in HTTP header without colon in request
31  */
32 
33 import java.io.*;
34 import java.net.*;
35 import java.util.Arrays;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors;
40 import java.util.stream.Collectors;
41 
42 import jdk.test.lib.net.URIBuilder;
43 import sun.net.www.MessageHeader;
44 import com.sun.net.httpserver.HttpContext;
45 import com.sun.net.httpserver.HttpExchange;
46 import com.sun.net.httpserver.HttpHandler;
47 import com.sun.net.httpserver.HttpServer;
48 
49 import static java.nio.charset.StandardCharsets.ISO_8859_1;
50 import static java.nio.charset.StandardCharsets.UTF_8;
51 
52 /*
53  * Test checks that MessageHeader with key != null and value == null is set correctly
54  * and printed according to HTTP standard in the format <key>: <value>
55  * */
56 public class B8185898 {
57 
58     static HttpServer server;
59     static final String RESPONSE_BODY = "Test response body";
60     static final String H1 = "X-header1";
61     static final String H2 = "X-header2";
62     static final String VALUE = "This test value should appear";
63     static int port;
64     static URL url;
65     static volatile Map<String, List<String>> headers;
66 
67     static class Handler implements HttpHandler {
68 
handle(HttpExchange t)69         public void handle(HttpExchange t) throws IOException {
70             InputStream is = t.getRequestBody();
71             InetSocketAddress rem = t.getRemoteAddress();
72             headers = t.getRequestHeaders();    // Get request headers on the server side
73             is.readAllBytes();
74             is.close();
75 
76             OutputStream os = t.getResponseBody();
77             t.sendResponseHeaders(200, RESPONSE_BODY.length());
78             os.write(RESPONSE_BODY.getBytes(UTF_8));
79             t.close();
80         }
81     }
82 
main(String[] args)83     public static void main(String[] args) throws Exception {
84         ExecutorService exec = Executors.newCachedThreadPool();
85         InetAddress loopback = InetAddress.getLoopbackAddress();
86 
87         try {
88             InetSocketAddress addr = new InetSocketAddress(loopback, 0);
89             server = HttpServer.create(addr, 100);
90             HttpHandler handler = new Handler();
91             HttpContext context = server.createContext("/", handler);
92             server.setExecutor(exec);
93             server.start();
94 
95             port = server.getAddress().getPort();
96             System.out.println("Server on port: " + port);
97             url = URIBuilder.newBuilder()
98                     .scheme("http")
99                     .loopback()
100                     .port(port)
101                     .path("/foo")
102                     .toURLUnchecked();
103             System.out.println("URL: " + url);
104             testMessageHeader();
105             testMessageHeaderMethods();
106             testURLConnectionMethods();
107         } finally {
108             server.stop(0);
109             System.out.println("After server shutdown");
110             exec.shutdown();
111         }
112     }
113 
114     // Test message header with malformed message header and fake request line
testMessageHeader()115     static void testMessageHeader() {
116         final String badHeader = "This is not a request line for HTTP/1.1";
117         final String fakeRequestLine = "This /is/a/fake/status/line HTTP/2.0";
118         final String expectedHeaders = fakeRequestLine + "\r\n"
119                 + H1 + ": " + VALUE + "\r\n"
120                 + H2 + ": " + VALUE + "\r\n"
121                 + badHeader + ":\r\n\r\n";
122 
123         MessageHeader header = new MessageHeader();
124         header.add(H1, VALUE);
125         header.add(H2, VALUE);
126         header.add(badHeader, null);
127         header.prepend(fakeRequestLine, null);
128         ByteArrayOutputStream out = new ByteArrayOutputStream();
129         header.print(new PrintStream(out));
130 
131         if (!out.toString().equals(expectedHeaders)) {
132             throw new AssertionError("FAILED: expected: "
133                     + expectedHeaders + "\nReceived: " + out.toString());
134         } else {
135             System.out.println("PASSED: ::print returned correct "
136                     + "status line and headers:\n" + out.toString());
137         }
138     }
139 
140     // Test MessageHeader::print, ::toString, implicitly testing that
141     // MessageHeader::mergeHeader formats headers correctly for responses
testMessageHeaderMethods()142     static void testMessageHeaderMethods() throws IOException {
143         // {{inputString1, expectedToString1, expectedPrint1}, {...}}
144         String[][] strings = {
145                 {"HTTP/1.1 200 OK\r\n"
146                         + "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"
147                         + "Connection: keep-alive\r\n"
148                         + "Host: 127.0.0.1:12345\r\n"
149                         + "User-agent: Java/12\r\n\r\nfoooo",
150                 "pairs: {null: HTTP/1.1 200 OK}"
151                         + "{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}"
152                         + "{Connection: keep-alive}"
153                         + "{Host: 127.0.0.1:12345}"
154                         + "{User-agent: Java/12}",
155                 "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"
156                         + "Connection: keep-alive\r\n"
157                         + "Host: 127.0.0.1:12345\r\n"
158                         + "User-agent: Java/12\r\n\r\n"},
159                 {"HTTP/1.1 200 OK\r\n"
160                         + "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"
161                         + "Connection: keep-alive\r\n"
162                         + "Host: 127.0.0.1:12345\r\n"
163                         + "User-agent: Java/12\r\n"
164                         + "X-Header:\r\n\r\n",
165                 "pairs: {null: HTTP/1.1 200 OK}"
166                         + "{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}"
167                         + "{Connection: keep-alive}"
168                         + "{Host: 127.0.0.1:12345}"
169                         + "{User-agent: Java/12}"
170                         + "{X-Header: }",
171                 "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"
172                         + "Connection: keep-alive\r\n"
173                         + "Host: 127.0.0.1:12345\r\n"
174                         + "User-agent: Java/12\r\n"
175                         + "X-Header: \r\n\r\n"},
176         };
177 
178         System.out.println("Test custom message headers");
179         for (String[] s : strings) {
180             // Test MessageHeader::toString
181             MessageHeader header = new MessageHeader(
182                     new ByteArrayInputStream(s[0].getBytes(ISO_8859_1)));
183             if (!header.toString().endsWith(s[1])) {
184                 throw new AssertionError("FAILED: expected: "
185                         + s[1] + "\nReceived: " + header);
186             } else {
187                 System.out.println("PASSED: ::toString returned correct "
188                         + "status line and headers:\n" + header);
189             }
190 
191             // Test MessageHeader::print
192             ByteArrayOutputStream out = new ByteArrayOutputStream();
193             header.print(new PrintStream(out));
194             if (!out.toString().equals(s[2])) {
195                 throw new AssertionError("FAILED: expected: "
196                         + s[2] + "\nReceived: " + out.toString());
197             } else {
198                 System.out.println("PASSED: ::print returned correct "
199                         + "status line and headers:\n" + out.toString());
200             }
201         }
202     }
203 
204     // Test methods URLConnection::getRequestProperties,
205     // ::getHeaderField, ::getHeaderFieldKey
testURLConnectionMethods()206     static void testURLConnectionMethods() throws IOException {
207         HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
208         urlConn.setRequestProperty(H1, "");
209         urlConn.setRequestProperty(H1, VALUE);
210         urlConn.setRequestProperty(H2, null);    // Expected to contain ':' between key and value
211         Map<String, List<String>> props = urlConn.getRequestProperties();
212         Map<String, List<String>> expectedMap = Map.of(
213                 H1, List.of(VALUE),
214                 H2, Arrays.asList((String) null));
215 
216         // Test request properties
217         System.out.println("Client request properties");
218         StringBuilder sb = new StringBuilder();
219         props.forEach((k, v) -> sb.append(k + ": "
220                 + v.stream().collect(Collectors.joining()) + "\n"));
221         System.out.println(sb);
222 
223         if (!props.equals(expectedMap)) {
224             throw new AssertionError("Unexpected properties returned: "
225                     + props);
226         } else {
227             System.out.println("Properties returned as expected");
228         }
229 
230         // Test header fields
231         String headerField = urlConn.getHeaderField(0);
232         if (!headerField.contains("200 OK")) {
233             throw new AssertionError("Expected headerField[0]: status line. "
234                     + "Received: " + headerField);
235         } else {
236             System.out.println("PASSED: headerField[0] contains status line: "
237                     + headerField);
238         }
239 
240         String headerFieldKey = urlConn.getHeaderFieldKey(0);
241         if (headerFieldKey != null) {
242             throw new AssertionError("Expected headerFieldKey[0]: null. "
243                     + "Received: " + headerFieldKey);
244         } else {
245             System.out.println("PASSED: headerFieldKey[0] is null");
246         }
247 
248         // Check that test request headers are included with correct format
249         try (
250                 BufferedReader in = new BufferedReader(
251                         new InputStreamReader(urlConn.getInputStream()))
252         ) {
253             if (!headers.keySet().contains(H1)) {
254                 throw new AssertionError("Expected key not found: "
255                         + H1 + ": " + VALUE);
256             } else if (!headers.get(H1).equals(List.of(VALUE))) {
257                 throw new AssertionError("Unexpected key-value pair: "
258                         + H1 + ": " + headers.get(H1));
259             } else {
260                 System.out.println("PASSED: " + H1 + " included in request headers");
261             }
262 
263             if (!headers.keySet().contains(H2)) {
264                 throw new AssertionError("Expected key not found: "
265                         + H2 + ": ");
266                 // Check that empty list is returned
267             } else if (!headers.get(H2).equals(List.of(""))) {
268                 throw new AssertionError("Unexpected key-value pair: "
269                         + H2 + ": " + headers.get(H2));
270             } else {
271                 System.out.println("PASSED: " + H2 + " included in request headers");
272             }
273 
274             String inputLine;
275             while ((inputLine = in.readLine()) != null) {
276                 System.out.println(inputLine);
277             }
278         }
279     }
280 }
281