|
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 package jdk.internal.net.http; |
|
25 |
|
26 import java.io.ByteArrayInputStream; |
|
27 import java.net.ProtocolException; |
|
28 import java.nio.ByteBuffer; |
|
29 import java.util.ArrayList; |
|
30 import java.util.Arrays; |
|
31 import java.util.Collections; |
|
32 import java.util.HashMap; |
|
33 import java.util.List; |
|
34 import java.util.Map; |
|
35 import java.util.stream.IntStream; |
|
36 import sun.net.www.MessageHeader; |
|
37 import org.testng.annotations.Test; |
|
38 import org.testng.annotations.DataProvider; |
|
39 import static java.lang.System.out; |
|
40 import static java.lang.String.format; |
|
41 import static java.nio.charset.StandardCharsets.US_ASCII; |
|
42 import static java.util.stream.Collectors.toList; |
|
43 import static org.testng.Assert.*; |
|
44 |
|
45 // Mostly verifies the "new" Http1HeaderParser returns the same results as the |
|
46 // tried and tested sun.net.www.MessageHeader. |
|
47 |
|
48 public class Http1HeaderParserTest { |
|
49 |
|
50 @DataProvider(name = "responses") |
|
51 public Object[][] responses() { |
|
52 List<String> responses = new ArrayList<>(); |
|
53 |
|
54 String[] basic = |
|
55 { "HTTP/1.1 200 OK\r\n\r\n", |
|
56 |
|
57 "HTTP/1.1 200 OK\r\n" + |
|
58 "Date: Mon, 15 Jan 2001 12:18:21 GMT\r\n" + |
|
59 "Server: Apache/1.3.14 (Unix)\r\n" + |
|
60 "Connection: close\r\n" + |
|
61 "Content-Type: text/html; charset=iso-8859-1\r\n" + |
|
62 "Content-Length: 10\r\n\r\n" + |
|
63 "123456789", |
|
64 |
|
65 "HTTP/1.1 200 OK\r\n" + |
|
66 "Content-Length: 9\r\n" + |
|
67 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + |
|
68 "XXXXX", |
|
69 |
|
70 "HTTP/1.1 200 OK\r\n" + |
|
71 "Content-Length: 9\r\n" + |
|
72 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + // more than one SP after ':' |
|
73 "XXXXX", |
|
74 |
|
75 "HTTP/1.1 200 OK\r\n" + |
|
76 "Content-Length:\t10\r\n" + |
|
77 "Content-Type:\ttext/html; charset=UTF-8\r\n\r\n" + // HT separator |
|
78 "XXXXX", |
|
79 |
|
80 "HTTP/1.1 200 OK\r\n" + |
|
81 "Content-Length:\t\t10\r\n" + |
|
82 "Content-Type:\t\ttext/html; charset=UTF-8\r\n\r\n" + // more than one HT after ':' |
|
83 "XXXXX", |
|
84 |
|
85 "HTTP/1.1 407 Proxy Authorization Required\r\n" + |
|
86 "Proxy-Authenticate: Basic realm=\"a fake realm\"\r\n\r\n", |
|
87 |
|
88 "HTTP/1.1 401 Unauthorized\r\n" + |
|
89 "WWW-Authenticate: Digest realm=\"wally land\" domain=/ " + |
|
90 "nonce=\"2B7F3A2B\" qop=\"auth\"\r\n\r\n", |
|
91 |
|
92 "HTTP/1.1 200 OK\r\n" + |
|
93 "X-Foo:\r\n\r\n", // no value |
|
94 |
|
95 "HTTP/1.1 200 OK\r\n" + |
|
96 "X-Foo:\r\n\r\n" + // no value, with response body |
|
97 "Some Response Body", |
|
98 |
|
99 "HTTP/1.1 200 OK\r\n" + |
|
100 "X-Foo:\r\n" + // no value, followed by another header |
|
101 "Content-Length: 10\r\n\r\n" + |
|
102 "Some Response Body", |
|
103 |
|
104 "HTTP/1.1 200 OK\r\n" + |
|
105 "X-Foo:\r\n" + // no value, followed by another header, with response body |
|
106 "Content-Length: 10\r\n\r\n", |
|
107 |
|
108 "HTTP/1.1 200 OK\r\n" + |
|
109 "X-Foo: chegar\r\n" + |
|
110 "X-Foo: dfuchs\r\n" + // same header appears multiple times |
|
111 "Content-Length: 0\r\n" + |
|
112 "X-Foo: michaelm\r\n" + |
|
113 "X-Foo: prappo\r\n\r\n", |
|
114 |
|
115 "HTTP/1.1 200 OK\r\n" + |
|
116 "X-Foo:\r\n" + // no value, same header appears multiple times |
|
117 "X-Foo: dfuchs\r\n" + |
|
118 "Content-Length: 0\r\n" + |
|
119 "X-Foo: michaelm\r\n" + |
|
120 "X-Foo: prappo\r\n\r\n", |
|
121 |
|
122 "HTTP/1.1 200 OK\r\n" + |
|
123 "Accept-Ranges: bytes\r\n" + |
|
124 "Cache-control: max-age=0, no-cache=\"set-cookie\"\r\n" + |
|
125 "Content-Length: 132868\r\n" + |
|
126 "Content-Type: text/html; charset=UTF-8\r\n" + |
|
127 "Date: Sun, 05 Nov 2017 22:24:03 GMT\r\n" + |
|
128 "Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips Communique/4.2.2\r\n" + |
|
129 "Set-Cookie: AWSELB=AF7927F5100F4202119876ED2436B5005EE;PATH=/;MAX-AGE=900\r\n" + |
|
130 "Vary: Host,Accept-Encoding,User-Agent\r\n" + |
|
131 "X-Mod-Pagespeed: 1.12.34.2-0\r\n" + |
|
132 "Connection: keep-alive\r\n\r\n" |
|
133 }; |
|
134 Arrays.stream(basic).forEach(responses::add); |
|
135 |
|
136 String[] foldingTemplate = |
|
137 { "HTTP/1.1 200 OK\r\n" + |
|
138 "Content-Length: 9\r\n" + |
|
139 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r' |
|
140 " charset=UTF-8\r\n" + // one preceding SP |
|
141 "Connection: close\r\n\r\n" + |
|
142 "XXYYZZAABBCCDDEE", |
|
143 |
|
144 "HTTP/1.1 200 OK\r\n" + |
|
145 "Content-Length: 19\r\n" + |
|
146 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r |
|
147 " charset=UTF-8\r\n" + // more than one preceding SP |
|
148 "Connection: keep-alive\r\n\r\n" + |
|
149 "XXYYZZAABBCCDDEEFFGG", |
|
150 |
|
151 "HTTP/1.1 200 OK\r\n" + |
|
152 "Content-Length: 999\r\n" + |
|
153 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r |
|
154 "\tcharset=UTF-8\r\n" + // one preceding HT |
|
155 "Connection: close\r\n\r\n" + |
|
156 "XXYYZZAABBCCDDEE", |
|
157 |
|
158 "HTTP/1.1 200 OK\r\n" + |
|
159 "Content-Length: 54\r\n" + |
|
160 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r |
|
161 "\t\t\tcharset=UTF-8\r\n" + // more than one preceding HT |
|
162 "Connection: keep-alive\r\n\r\n" + |
|
163 "XXYYZZAABBCCDDEEFFGG", |
|
164 |
|
165 "HTTP/1.1 200 OK\r\n" + |
|
166 "Content-Length: -1\r\n" + |
|
167 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r |
|
168 "\t \t \tcharset=UTF-8\r\n" + // mix of preceding HT and SP |
|
169 "Connection: keep-alive\r\n\r\n" + |
|
170 "XXYYZZAABBCCDDEEFFGGHH", |
|
171 |
|
172 "HTTP/1.1 200 OK\r\n" + |
|
173 "Content-Length: 65\r\n" + |
|
174 "Content-Type: text/html;$NEWLINE" + // folding field-value with '\n'|'\r |
|
175 " \t \t charset=UTF-8\r\n" + // mix of preceding SP and HT |
|
176 "Connection: keep-alive\r\n\r\n" + |
|
177 "XXYYZZAABBCCDDEEFFGGHHII", |
|
178 |
|
179 "HTTP/1.1 401 Unauthorized\r\n" + |
|
180 "WWW-Authenticate: Digest realm=\"wally land\"," |
|
181 +"$NEWLINE domain=/," |
|
182 +"$NEWLINE nonce=\"2B7F3A2B\"," |
|
183 +"$NEWLINE\tqop=\"auth\"\r\n\r\n", |
|
184 |
|
185 }; |
|
186 for (String newLineChar : new String[] { "\n", "\r", "\r\n" }) { |
|
187 for (String template : foldingTemplate) |
|
188 responses.add(template.replace("$NEWLINE", newLineChar)); |
|
189 } |
|
190 |
|
191 String[] bad = // much of this is to retain parity with legacy MessageHeaders |
|
192 { "HTTP/1.1 200 OK\r\n" + |
|
193 "Connection:\r\n\r\n", // empty value, no body |
|
194 |
|
195 "HTTP/1.1 200 OK\r\n" + |
|
196 "Connection:\r\n\r\n" + // empty value, with body |
|
197 "XXXXX", |
|
198 |
|
199 "HTTP/1.1 200 OK\r\n" + |
|
200 ": no header\r\n\r\n", // no/empty header-name, no body, no following header |
|
201 |
|
202 "HTTP/1.1 200 OK\r\n" + |
|
203 ": no; header\r\n" + // no/empty header-name, no body, following header |
|
204 "Content-Length: 65\r\n\r\n", |
|
205 |
|
206 "HTTP/1.1 200 OK\r\n" + |
|
207 ": no header\r\n" + // no/empty header-name |
|
208 "Content-Length: 65\r\n\r\n" + |
|
209 "XXXXX", |
|
210 |
|
211 "HTTP/1.1 200 OK\r\n" + |
|
212 ": no header\r\n\r\n" + // no/empty header-name, followed by header |
|
213 "XXXXX", |
|
214 |
|
215 "HTTP/1.1 200 OK\r\n" + |
|
216 "Conte\r" + |
|
217 " nt-Length: 9\r\n" + // fold/bad header name ??? |
|
218 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + |
|
219 "XXXXX", |
|
220 |
|
221 "HTTP/1.1 200 OK\r\n" + |
|
222 "Conte\r" + |
|
223 "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space |
|
224 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + |
|
225 "XXXXXYYZZ", |
|
226 |
|
227 "HTTP/1.0 404 Not Found\r\n" + |
|
228 "header-without-colon\r\n\r\n", |
|
229 |
|
230 "HTTP/1.0 404 Not Found\r\n" + |
|
231 "header-without-colon\r\n\r\n" + |
|
232 "SOMEBODY", |
|
233 |
|
234 }; |
|
235 Arrays.stream(bad).forEach(responses::add); |
|
236 |
|
237 return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); |
|
238 } |
|
239 |
|
240 @Test(dataProvider = "responses") |
|
241 public void verifyHeaders(String respString) throws Exception { |
|
242 byte[] bytes = respString.getBytes(US_ASCII); |
|
243 ByteArrayInputStream bais = new ByteArrayInputStream(bytes); |
|
244 MessageHeader m = new MessageHeader(bais); |
|
245 Map<String,List<String>> messageHeaderMap = m.getHeaders(); |
|
246 int available = bais.available(); |
|
247 |
|
248 Http1HeaderParser decoder = new Http1HeaderParser(); |
|
249 ByteBuffer b = ByteBuffer.wrap(bytes); |
|
250 decoder.parse(b); |
|
251 Map<String,List<String>> decoderMap1 = decoder.headers().map(); |
|
252 assertEquals(available, b.remaining(), |
|
253 "stream available not equal to remaining"); |
|
254 |
|
255 // assert status-line |
|
256 String statusLine1 = messageHeaderMap.get(null).get(0); |
|
257 String statusLine2 = decoder.statusLine(); |
|
258 if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line |
|
259 assertEquals(statusLine1, statusLine2, "Status-line not equal"); |
|
260 } else { |
|
261 assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1."); |
|
262 } |
|
263 |
|
264 // remove the null'th entry with is the status-line |
|
265 Map<String,List<String>> map = new HashMap<>(); |
|
266 for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) { |
|
267 if (e.getKey() != null) { |
|
268 map.put(e.getKey(), e.getValue()); |
|
269 } |
|
270 } |
|
271 messageHeaderMap = map; |
|
272 |
|
273 assertHeadersEqual(messageHeaderMap, decoderMap1, |
|
274 "messageHeaderMap not equal to decoderMap1"); |
|
275 |
|
276 // byte at a time |
|
277 decoder = new Http1HeaderParser(); |
|
278 List<ByteBuffer> buffers = IntStream.range(0, bytes.length) |
|
279 .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1)) |
|
280 .collect(toList()); |
|
281 while (decoder.parse(buffers.remove(0)) != true); |
|
282 Map<String,List<String>> decoderMap2 = decoder.headers().map(); |
|
283 assertEquals(available, buffers.size(), |
|
284 "stream available not equals to remaining buffers"); |
|
285 assertEquals(decoderMap1, decoderMap2, "decoder maps not equal"); |
|
286 } |
|
287 |
|
288 @DataProvider(name = "errors") |
|
289 public Object[][] errors() { |
|
290 List<String> responses = new ArrayList<>(); |
|
291 |
|
292 // These responses are parsed, somewhat, by MessageHeaders but give |
|
293 // nonsensible results. They, correctly, fail with the Http1HeaderParser. |
|
294 String[] bad = |
|
295 {// "HTTP/1.1 402 Payment Required\r\n" + |
|
296 // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete |
|
297 |
|
298 "HTTP/1.1 402 Payment Required\r\n" + |
|
299 "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG", |
|
300 |
|
301 "HTTP/1.1 200OK\r\n\rT", |
|
302 |
|
303 "HTTP/1.1 200OK\rT", |
|
304 }; |
|
305 Arrays.stream(bad).forEach(responses::add); |
|
306 |
|
307 return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); |
|
308 } |
|
309 |
|
310 @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class) |
|
311 public void errors(String respString) throws ProtocolException { |
|
312 byte[] bytes = respString.getBytes(US_ASCII); |
|
313 Http1HeaderParser decoder = new Http1HeaderParser(); |
|
314 ByteBuffer b = ByteBuffer.wrap(bytes); |
|
315 decoder.parse(b); |
|
316 } |
|
317 |
|
318 void assertHeadersEqual(Map<String,List<String>> expected, |
|
319 Map<String,List<String>> actual, |
|
320 String msg) { |
|
321 |
|
322 if (expected.equals(actual)) |
|
323 return; |
|
324 |
|
325 assertEquals(expected.size(), actual.size(), |
|
326 format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.", |
|
327 msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual))); |
|
328 |
|
329 for (Map.Entry<String,List<String>> e : expected.entrySet()) { |
|
330 String key = e.getKey(); |
|
331 List<String> values = e.getValue(); |
|
332 |
|
333 boolean found = false; |
|
334 for (Map.Entry<String,List<String>> other: actual.entrySet()) { |
|
335 if (key.equalsIgnoreCase(other.getKey())) { |
|
336 found = true; |
|
337 List<String> otherValues = other.getValue(); |
|
338 assertEquals(values.size(), otherValues.size(), |
|
339 format("%s. Expected list size %d, actual size %s", |
|
340 msg, values.size(), otherValues.size())); |
|
341 if (!(values.containsAll(otherValues) && otherValues.containsAll(values))) |
|
342 assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues)); |
|
343 break; |
|
344 } |
|
345 } |
|
346 assertTrue(found, format("header name, %s, not found in %s", key, actual)); |
|
347 } |
|
348 } |
|
349 |
|
350 static String mapToString(Map<String,List<String>> map) { |
|
351 StringBuilder sb = new StringBuilder(); |
|
352 List<String> sortedKeys = new ArrayList(map.keySet()); |
|
353 Collections.sort(sortedKeys); |
|
354 for (String key : sortedKeys) { |
|
355 List<String> values = map.get(key); |
|
356 sb.append("\n\t" + key + " | " + values); |
|
357 } |
|
358 return sb.toString(); |
|
359 } |
|
360 |
|
361 // --- |
|
362 |
|
363 /* Main entry point for standalone testing of the main functional test. */ |
|
364 public static void main(String... args) throws Exception { |
|
365 Http1HeaderParserTest test = new Http1HeaderParserTest(); |
|
366 int count = 0; |
|
367 for (Object[] objs : test.responses()) { |
|
368 out.println("Testing " + count++ + ", " + objs[0]); |
|
369 test.verifyHeaders((String) objs[0]); |
|
370 } |
|
371 for (Object[] objs : test.errors()) { |
|
372 out.println("Testing " + count++ + ", " + objs[0]); |
|
373 try { |
|
374 test.errors((String) objs[0]); |
|
375 throw new RuntimeException("Expected ProtocolException for " + objs[0]); |
|
376 } catch (ProtocolException expected) { /* Ok */ } |
|
377 } |
|
378 } |
|
379 } |