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 package jdk.incubator.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 for (String newLineChar : new String[] { "\n", "\r" }) { 180 for (String template : foldingTemplate) 181 responses.add(template.replace("$NEWLINE", newLineChar)); 182 } 183 184 String[] bad = // much of this is to retain parity with legacy MessageHeaders 185 { "HTTP/1.1 200 OK\r\n" + 186 "Connection:\r\n\r\n", // empty value, no body 187 188 "HTTP/1.1 200 OK\r\n" + 189 "Connection:\r\n\r\n" + // empty value, with body 190 "XXXXX", 191 192 "HTTP/1.1 200 OK\r\n" + 193 ": no header\r\n\r\n", // no/empty header-name, no body, no following header 194 195 "HTTP/1.1 200 OK\r\n" + 196 ": no; header\r\n" + // no/empty header-name, no body, following header 197 "Content-Length: 65\r\n\r\n", 198 199 "HTTP/1.1 200 OK\r\n" + 200 ": no header\r\n" + // no/empty header-name 201 "Content-Length: 65\r\n\r\n" + 202 "XXXXX", 203 204 "HTTP/1.1 200 OK\r\n" + 205 ": no header\r\n\r\n" + // no/empty header-name, followed by header 206 "XXXXX", 207 208 "HTTP/1.1 200 OK\r\n" + 209 "Conte\r" + 210 " nt-Length: 9\r\n" + // fold/bad header name ??? 211 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + 212 "XXXXX", 213 214 "HTTP/1.1 200 OK\r\n" + 215 "Conte\r" + 216 "nt-Length: 9\r\n" + // fold/bad header name ??? without preceding space 217 "Content-Type: text/html; charset=UTF-8\r\n\r\n" + 218 "XXXXXYYZZ", 219 220 "HTTP/1.0 404 Not Found\r\n" + 221 "header-without-colon\r\n\r\n", 222 223 "HTTP/1.0 404 Not Found\r\n" + 224 "header-without-colon\r\n\r\n" + 225 "SOMEBODY", 226 227 }; 228 Arrays.stream(bad).forEach(responses::add); 229 230 return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); 231 } 232 233 @Test(dataProvider = "responses") 234 public void verifyHeaders(String respString) throws Exception { 235 byte[] bytes = respString.getBytes(US_ASCII); 236 ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 237 MessageHeader m = new MessageHeader(bais); 238 Map<String,List<String>> messageHeaderMap = m.getHeaders(); 239 int available = bais.available(); 240 241 Http1HeaderParser decoder = new Http1HeaderParser(); 242 ByteBuffer b = ByteBuffer.wrap(bytes); 243 decoder.parse(b); 244 Map<String,List<String>> decoderMap1 = decoder.headers().map(); 245 assertEquals(available, b.remaining(), 246 "stream available not equal to remaining"); 247 248 // assert status-line 249 String statusLine1 = messageHeaderMap.get(null).get(0); 250 String statusLine2 = decoder.statusLine(); 251 if (statusLine1.startsWith("HTTP")) {// skip the case where MH's messes up the status-line 252 assertEquals(statusLine1, statusLine2, "Status-line not equal"); 253 } else { 254 assertTrue(statusLine2.startsWith("HTTP/1."), "Status-line not HTTP/1."); 255 } 256 257 // remove the null'th entry with is the status-line 258 Map<String,List<String>> map = new HashMap<>(); 259 for (Map.Entry<String,List<String>> e : messageHeaderMap.entrySet()) { 260 if (e.getKey() != null) { 261 map.put(e.getKey(), e.getValue()); 262 } 263 } 264 messageHeaderMap = map; 265 266 assertHeadersEqual(messageHeaderMap, decoderMap1, 267 "messageHeaderMap not equal to decoderMap1"); 268 269 // byte at a time 270 decoder = new Http1HeaderParser(); 271 List<ByteBuffer> buffers = IntStream.range(0, bytes.length) 272 .mapToObj(i -> ByteBuffer.wrap(bytes, i, 1)) 273 .collect(toList()); 274 while (decoder.parse(buffers.remove(0)) != true); 275 Map<String,List<String>> decoderMap2 = decoder.headers().map(); 276 assertEquals(available, buffers.size(), 277 "stream available not equals to remaining buffers"); 278 assertEquals(decoderMap1, decoderMap2, "decoder maps not equal"); 279 } 280 281 @DataProvider(name = "errors") 282 public Object[][] errors() { 283 List<String> responses = new ArrayList<>(); 284 285 // These responses are parsed, somewhat, by MessageHeaders but give 286 // nonsensible results. They, correctly, fail with the Http1HeaderParser. 287 String[] bad = 288 {// "HTTP/1.1 402 Payment Required\r\n" + 289 // "Content-Length: 65\r\n\r", // missing trailing LF //TODO: incomplete 290 291 "HTTP/1.1 402 Payment Required\r\n" + 292 "Content-Length: 65\r\n\rT\r\n\r\nGGGGGG", 293 294 "HTTP/1.1 200OK\r\n\rT", 295 296 "HTTP/1.1 200OK\rT", 297 }; 298 Arrays.stream(bad).forEach(responses::add); 299 300 return responses.stream().map(p -> new Object[] { p }).toArray(Object[][]::new); 301 } 302 303 @Test(dataProvider = "errors", expectedExceptions = ProtocolException.class) 304 public void errors(String respString) throws ProtocolException { 305 byte[] bytes = respString.getBytes(US_ASCII); 306 Http1HeaderParser decoder = new Http1HeaderParser(); 307 ByteBuffer b = ByteBuffer.wrap(bytes); 308 decoder.parse(b); 309 } 310 311 void assertHeadersEqual(Map<String,List<String>> expected, 312 Map<String,List<String>> actual, 313 String msg) { 314 315 if (expected.equals(actual)) 316 return; 317 318 assertEquals(expected.size(), actual.size(), 319 format("%s. Expected size %d, actual size %s. %nexpected= %s,%n actual=%s.", 320 msg, expected.size(), actual.size(), mapToString(expected), mapToString(actual))); 321 322 for (Map.Entry<String,List<String>> e : expected.entrySet()) { 323 String key = e.getKey(); 324 List<String> values = e.getValue(); 325 326 boolean found = false; 327 for (Map.Entry<String,List<String>> other: actual.entrySet()) { 328 if (key.equalsIgnoreCase(other.getKey())) { 329 found = true; 330 List<String> otherValues = other.getValue(); 331 assertEquals(values.size(), otherValues.size(), 332 format("%s. Expected list size %d, actual size %s", 333 msg, values.size(), otherValues.size())); 334 if (!values.containsAll(otherValues) && otherValues.containsAll(values)) 335 assertTrue(false, format("Lists are unequal [%s] [%s]", values, otherValues)); 336 break; 337 } 338 } 339 assertTrue(found, format("header name, %s, not found in %s", key, actual)); 340 } 341 } 342 343 static String mapToString(Map<String,List<String>> map) { 344 StringBuilder sb = new StringBuilder(); 345 List<String> sortedKeys = new ArrayList(map.keySet()); 346 Collections.sort(sortedKeys); 347 for (String key : sortedKeys) { 348 List<String> values = map.get(key); 349 sb.append("\n\t" + key + " | " + values); 350 } 351 return sb.toString(); 352 } 353 354 // --- 355 356 /* Main entry point for standalone testing of the main functional test. */ 357 public static void main(String... args) throws Exception { 358 Http1HeaderParserTest test = new Http1HeaderParserTest(); 359 int count = 0; 360 for (Object[] objs : test.responses()) { 361 out.println("Testing " + count++ + ", " + objs[0]); 362 test.verifyHeaders((String) objs[0]); 363 } 364 for (Object[] objs : test.errors()) { 365 out.println("Testing " + count++ + ", " + objs[0]); 366 try { 367 test.errors((String) objs[0]); 368 throw new RuntimeException("Expected ProtocolException for " + objs[0]); 369 } catch (ProtocolException expected) { /* Ok */ } 370 } 371 } 372 }