test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/Http1HeaderParserTest.java
branchhttp-client-branch
changeset 56092 fd85b2bf2b0d
parent 56089 42208b2f224e
child 56451 9585061fdb04
equal deleted inserted replaced
56091:aedd6133e7a0 56092:fd85b2bf2b0d
       
     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 }