|
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 java.net.http.internal.hpack; |
|
24 |
|
25 import org.testng.annotations.Test; |
|
26 import java.net.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 java.net.http.internal.hpack.TestHelper.assertExceptionMessageContains; |
|
40 import static java.net.http.internal.hpack.TestHelper.assertThrows; |
|
41 import static java.net.http.internal.hpack.TestHelper.assertVoidThrows; |
|
42 import static java.net.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 } |