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 }