1 /*
   2  * Copyright (c) 2014, 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 package jdk.incubator.http.internal.hpack;
  24 
  25 import org.testng.annotations.Test;
  26 import jdk.incubator.http.internal.hpack.HeaderTable.HeaderField;
  27 
  28 import java.nio.charset.StandardCharsets;
  29 import java.util.Collections;
  30 import java.util.HashMap;
  31 import java.util.Locale;
  32 import java.util.Map;
  33 import java.util.Random;
  34 import java.util.regex.Matcher;
  35 import java.util.regex.Pattern;
  36 
  37 import static java.lang.String.format;
  38 import static org.testng.Assert.assertEquals;
  39 import static jdk.incubator.http.internal.hpack.TestHelper.assertExceptionMessageContains;
  40 import static jdk.incubator.http.internal.hpack.TestHelper.assertThrows;
  41 import static jdk.incubator.http.internal.hpack.TestHelper.assertVoidThrows;
  42 import static jdk.incubator.http.internal.hpack.TestHelper.newRandom;
  43 
  44 public class HeaderTableTest {
  45 
  46     //
  47     // https://tools.ietf.org/html/rfc7541#appendix-A
  48     //
  49     // @formatter:off
  50     private static final String SPEC =
  51        "          | 1     | :authority                  |               |\n" +
  52        "          | 2     | :method                     | GET           |\n" +
  53        "          | 3     | :method                     | POST          |\n" +
  54        "          | 4     | :path                       | /             |\n" +
  55        "          | 5     | :path                       | /index.html   |\n" +
  56        "          | 6     | :scheme                     | http          |\n" +
  57        "          | 7     | :scheme                     | https         |\n" +
  58        "          | 8     | :status                     | 200           |\n" +
  59        "          | 9     | :status                     | 204           |\n" +
  60        "          | 10    | :status                     | 206           |\n" +
  61        "          | 11    | :status                     | 304           |\n" +
  62        "          | 12    | :status                     | 400           |\n" +
  63        "          | 13    | :status                     | 404           |\n" +
  64        "          | 14    | :status                     | 500           |\n" +
  65        "          | 15    | accept-charset              |               |\n" +
  66        "          | 16    | accept-encoding             | gzip, deflate |\n" +
  67        "          | 17    | accept-language             |               |\n" +
  68        "          | 18    | accept-ranges               |               |\n" +
  69        "          | 19    | accept                      |               |\n" +
  70        "          | 20    | access-control-allow-origin |               |\n" +
  71        "          | 21    | age                         |               |\n" +
  72        "          | 22    | allow                       |               |\n" +
  73        "          | 23    | authorization               |               |\n" +
  74        "          | 24    | cache-control               |               |\n" +
  75        "          | 25    | content-disposition         |               |\n" +
  76        "          | 26    | content-encoding            |               |\n" +
  77        "          | 27    | content-language            |               |\n" +
  78        "          | 28    | content-length              |               |\n" +
  79        "          | 29    | content-location            |               |\n" +
  80        "          | 30    | content-range               |               |\n" +
  81        "          | 31    | content-type                |               |\n" +
  82        "          | 32    | cookie                      |               |\n" +
  83        "          | 33    | date                        |               |\n" +
  84        "          | 34    | etag                        |               |\n" +
  85        "          | 35    | expect                      |               |\n" +
  86        "          | 36    | expires                     |               |\n" +
  87        "          | 37    | from                        |               |\n" +
  88        "          | 38    | host                        |               |\n" +
  89        "          | 39    | if-match                    |               |\n" +
  90        "          | 40    | if-modified-since           |               |\n" +
  91        "          | 41    | if-none-match               |               |\n" +
  92        "          | 42    | if-range                    |               |\n" +
  93        "          | 43    | if-unmodified-since         |               |\n" +
  94        "          | 44    | last-modified               |               |\n" +
  95        "          | 45    | link                        |               |\n" +
  96        "          | 46    | location                    |               |\n" +
  97        "          | 47    | max-forwards                |               |\n" +
  98        "          | 48    | proxy-authenticate          |               |\n" +
  99        "          | 49    | proxy-authorization         |               |\n" +
 100        "          | 50    | range                       |               |\n" +
 101        "          | 51    | referer                     |               |\n" +
 102        "          | 52    | refresh                     |               |\n" +
 103        "          | 53    | retry-after                 |               |\n" +
 104        "          | 54    | server                      |               |\n" +
 105        "          | 55    | set-cookie                  |               |\n" +
 106        "          | 56    | strict-transport-security   |               |\n" +
 107        "          | 57    | transfer-encoding           |               |\n" +
 108        "          | 58    | user-agent                  |               |\n" +
 109        "          | 59    | vary                        |               |\n" +
 110        "          | 60    | via                         |               |\n" +
 111        "          | 61    | www-authenticate            |               |\n";
 112     // @formatter:on
 113 
 114     private static final int STATIC_TABLE_LENGTH = createStaticEntries().size();
 115     private final Random rnd = newRandom();
 116 
 117     @Test
 118     public void staticData() {
 119         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 120         Map<Integer, HeaderField> staticHeaderFields = createStaticEntries();
 121 
 122         Map<String, Integer> minimalIndexes = new HashMap<>();
 123 
 124         for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) {
 125             Integer idx = e.getKey();
 126             String hName = e.getValue().name;
 127             Integer midx = minimalIndexes.get(hName);
 128             if (midx == null) {
 129                 minimalIndexes.put(hName, idx);
 130             } else {
 131                 minimalIndexes.put(hName, Math.min(idx, midx));
 132             }
 133         }
 134 
 135         staticHeaderFields.entrySet().forEach(
 136                 e -> {
 137                     // lookup
 138                     HeaderField actualHeaderField = table.get(e.getKey());
 139                     HeaderField expectedHeaderField = e.getValue();
 140                     assertEquals(actualHeaderField, expectedHeaderField);
 141 
 142                     // reverse lookup (name, value)
 143                     String hName = expectedHeaderField.name;
 144                     String hValue = expectedHeaderField.value;
 145                     int expectedIndex = e.getKey();
 146                     int actualIndex = table.indexOf(hName, hValue);
 147 
 148                     assertEquals(actualIndex, expectedIndex);
 149 
 150                     // reverse lookup (name)
 151                     int expectedMinimalIndex = minimalIndexes.get(hName);
 152                     int actualMinimalIndex = table.indexOf(hName, "blah-blah");
 153 
 154                     assertEquals(-actualMinimalIndex, expectedMinimalIndex);
 155                 }
 156         );
 157     }
 158 
 159     @Test
 160     public void constructorSetsMaxSize() {
 161         int size = rnd.nextInt(64);
 162         HeaderTable t = new HeaderTable(size, HPACK.getLogger());
 163         assertEquals(t.size(), 0);
 164         assertEquals(t.maxSize(), size);
 165     }
 166 
 167     @Test
 168     public void negativeMaximumSize() {
 169         int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1]
 170         IllegalArgumentException e =
 171                 assertVoidThrows(IllegalArgumentException.class,
 172                         () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize));
 173         assertExceptionMessageContains(e, "maxSize");
 174     }
 175 
 176     @Test
 177     public void zeroMaximumSize() {
 178         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 179         table.setMaxSize(0);
 180         assertEquals(table.maxSize(), 0);
 181     }
 182 
 183     @Test
 184     public void negativeIndex() {
 185         int idx = -(rnd.nextInt(256) + 1); // [-256, -1]
 186         IndexOutOfBoundsException e =
 187                 assertVoidThrows(IndexOutOfBoundsException.class,
 188                         () -> new HeaderTable(0, HPACK.getLogger()).get(idx));
 189         assertExceptionMessageContains(e, "index");
 190     }
 191 
 192     @Test
 193     public void zeroIndex() {
 194         IndexOutOfBoundsException e =
 195                 assertThrows(IndexOutOfBoundsException.class,
 196                         () -> new HeaderTable(0, HPACK.getLogger()).get(0));
 197         assertExceptionMessageContains(e, "index");
 198     }
 199 
 200     @Test
 201     public void length() {
 202         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 203         assertEquals(table.length(), STATIC_TABLE_LENGTH);
 204     }
 205 
 206     @Test
 207     public void indexOutsideStaticRange() {
 208         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 209         int idx = table.length() + (rnd.nextInt(256) + 1);
 210         IndexOutOfBoundsException e =
 211                 assertThrows(IndexOutOfBoundsException.class,
 212                         () -> table.get(idx));
 213         assertExceptionMessageContains(e, "index");
 214     }
 215 
 216     @Test
 217     public void entryPutAfterStaticArea() {
 218         HeaderTable table = new HeaderTable(256, HPACK.getLogger());
 219         int idx = table.length() + 1;
 220         assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx));
 221 
 222         byte[] bytes = new byte[32];
 223         rnd.nextBytes(bytes);
 224         String name = new String(bytes, StandardCharsets.ISO_8859_1);
 225         String value = "custom-value";
 226 
 227         table.put(name, value);
 228         HeaderField f = table.get(idx);
 229         assertEquals(name, f.name);
 230         assertEquals(value, f.value);
 231     }
 232 
 233     @Test
 234     public void staticTableHasZeroSize() {
 235         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 236         assertEquals(0, table.size());
 237     }
 238 
 239     @Test
 240     public void lowerIndexPriority() {
 241         HeaderTable table = new HeaderTable(256, HPACK.getLogger());
 242         int oldLength = table.length();
 243         table.put("bender", "rodriguez");
 244         table.put("bender", "rodriguez");
 245         table.put("bender", "rodriguez");
 246 
 247         assertEquals(table.length(), oldLength + 3); // more like an assumption
 248         int i = table.indexOf("bender", "rodriguez");
 249         assertEquals(oldLength + 1, i);
 250     }
 251 
 252     @Test
 253     public void lowerIndexPriority2() {
 254         HeaderTable table = new HeaderTable(256, HPACK.getLogger());
 255         int oldLength = table.length();
 256         int idx = rnd.nextInt(oldLength) + 1;
 257         HeaderField f = table.get(idx);
 258         table.put(f.name, f.value);
 259         assertEquals(table.length(), oldLength + 1);
 260         int i = table.indexOf(f.name, f.value);
 261         assertEquals(idx, i);
 262     }
 263 
 264     // TODO: negative indexes check
 265     // TODO: ensure full table clearance when adding huge header field
 266     // TODO: ensure eviction deletes minimum needed entries, not more
 267 
 268     @Test
 269     public void fifo() {
 270         // Let's add a series of header fields
 271         int NUM_HEADERS = 32;
 272         HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger());
 273         //                                ^   ^
 274         //                   entry overhead   symbols per entry (max 2x2 digits)
 275         for (int i = 1; i <= NUM_HEADERS; i++) {
 276             String s = String.valueOf(i);
 277             t.put(s, s);
 278         }
 279         // They MUST appear in a FIFO order:
 280         //   newer entries are at lower indexes
 281         //   older entries are at higher indexes
 282         for (int j = 1; j <= NUM_HEADERS; j++) {
 283             HeaderField f = t.get(STATIC_TABLE_LENGTH + j);
 284             int actualName = Integer.parseInt(f.name);
 285             int expectedName = NUM_HEADERS - j + 1;
 286             assertEquals(expectedName, actualName);
 287         }
 288         // Entries MUST be evicted in the order they were added:
 289         //   the newer the entry the later it is evicted
 290         for (int k = 1; k <= NUM_HEADERS; k++) {
 291             HeaderField f = t.evictEntry();
 292             assertEquals(String.valueOf(k), f.name);
 293         }
 294     }
 295 
 296     @Test
 297     public void indexOf() {
 298         // Let's put a series of header fields
 299         int NUM_HEADERS = 32;
 300         HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger());
 301         //                                ^   ^
 302         //                   entry overhead   symbols per entry (max 2x2 digits)
 303         for (int i = 1; i <= NUM_HEADERS; i++) {
 304             String s = String.valueOf(i);
 305             t.put(s, s);
 306         }
 307         // and verify indexOf (reverse lookup) returns correct indexes for
 308         // full lookup
 309         for (int j = 1; j <= NUM_HEADERS; j++) {
 310             String s = String.valueOf(j);
 311             int actualIndex = t.indexOf(s, s);
 312             int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1;
 313             assertEquals(expectedIndex, actualIndex);
 314         }
 315         // as well as for just a name lookup
 316         for (int j = 1; j <= NUM_HEADERS; j++) {
 317             String s = String.valueOf(j);
 318             int actualIndex = t.indexOf(s, "blah");
 319             int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1);
 320             assertEquals(expectedIndex, actualIndex);
 321         }
 322         // lookup for non-existent name returns 0
 323         assertEquals(0, t.indexOf("chupacabra", "1"));
 324     }
 325 
 326     @Test
 327     public void testToString() {
 328         testToString0();
 329     }
 330 
 331     @Test
 332     public void testToStringDifferentLocale() {
 333         Locale locale = Locale.getDefault();
 334         Locale.setDefault(Locale.FRENCH);
 335         try {
 336             String s = format("%.1f", 3.1);
 337             assertEquals("3,1", s); // assumption of the test, otherwise the test is useless
 338             testToString0();
 339         } finally {
 340             Locale.setDefault(locale);
 341         }
 342     }
 343 
 344     private void testToString0() {
 345         HeaderTable table = new HeaderTable(0, HPACK.getLogger());
 346         {
 347             int maxSize = 2048;
 348             table.setMaxSize(maxSize);
 349             String expected = format(
 350                     "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
 351                     0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0);
 352             assertEquals(expected, table.toString());
 353         }
 354 
 355         {
 356             String name = "custom-name";
 357             String value = "custom-value";
 358             int size = 512;
 359 
 360             table.setMaxSize(size);
 361             table.put(name, value);
 362             String s = table.toString();
 363 
 364             int used = name.length() + value.length() + 32;
 365             double ratio = used * 100.0 / size;
 366 
 367             String expected = format(
 368                     "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
 369                     1, STATIC_TABLE_LENGTH + 1, used, size, ratio);
 370             assertEquals(expected, s);
 371         }
 372 
 373         {
 374             table.setMaxSize(78);
 375             table.put(":method", "");
 376             table.put(":status", "");
 377             String s = table.toString();
 378             String expected =
 379                     format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)",
 380                            2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0);
 381             assertEquals(expected, s);
 382         }
 383     }
 384 
 385     @Test
 386     public void stateString() {
 387         HeaderTable table = new HeaderTable(256, HPACK.getLogger());
 388         table.put("custom-key", "custom-header");
 389         // @formatter:off
 390         assertEquals("[  1] (s =  55) custom-key: custom-header\n" +
 391                      "      Table size:  55", table.getStateString());
 392         // @formatter:on
 393     }
 394 
 395     private static Map<Integer, HeaderField> createStaticEntries() {
 396         Pattern line = Pattern.compile(
 397                 "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|");
 398         Matcher m = line.matcher(SPEC);
 399         Map<Integer, HeaderField> result = new HashMap<>();
 400         while (m.find()) {
 401             int index = Integer.parseInt(m.group("index"));
 402             String name = m.group("name");
 403             String value = m.group("value");
 404             HeaderField f = new HeaderField(name, value);
 405             result.put(index, f);
 406         }
 407         return Collections.unmodifiableMap(result); // lol
 408     }
 409 }