|
1 /* |
|
2 * Copyright (c) 2014, 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. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 package jdk.internal.net.http.hpack; |
|
26 |
|
27 import jdk.internal.net.http.hpack.HPACK.Logger; |
|
28 |
|
29 import java.nio.ByteBuffer; |
|
30 import java.nio.ReadOnlyBufferException; |
|
31 import java.util.LinkedList; |
|
32 import java.util.List; |
|
33 import java.util.concurrent.atomic.AtomicLong; |
|
34 |
|
35 import static java.lang.String.format; |
|
36 import static java.util.Objects.requireNonNull; |
|
37 import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA; |
|
38 import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL; |
|
39 |
|
40 /** |
|
41 * Encodes headers to their binary representation. |
|
42 * |
|
43 * <p> Typical lifecycle looks like this: |
|
44 * |
|
45 * <p> {@link #Encoder(int) new Encoder} |
|
46 * ({@link #setMaxCapacity(int) setMaxCapacity}? |
|
47 * {@link #encode(ByteBuffer) encode})* |
|
48 * |
|
49 * <p> Suppose headers are represented by {@code Map<String, List<String>>}. |
|
50 * A supplier and a consumer of {@link ByteBuffer}s in forms of |
|
51 * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively. |
|
52 * Then to encode headers, the following approach might be used: |
|
53 * |
|
54 * <pre>{@code |
|
55 * for (Map.Entry<String, List<String>> h : headers.entrySet()) { |
|
56 * String name = h.getKey(); |
|
57 * for (String value : h.getValue()) { |
|
58 * encoder.header(name, value); // Set up header |
|
59 * boolean encoded; |
|
60 * do { |
|
61 * ByteBuffer b = buffersSupplier.get(); |
|
62 * encoded = encoder.encode(b); // Encode the header |
|
63 * buffersConsumer.accept(b); |
|
64 * } while (!encoded); |
|
65 * } |
|
66 * } |
|
67 * }</pre> |
|
68 * |
|
69 * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a> |
|
70 * how an encoder is to be implemented, a default implementation is provided by |
|
71 * the method {@link #header(CharSequence, CharSequence, boolean)}. |
|
72 * |
|
73 * <p> To provide a custom encoding implementation, {@code Encoder} has to be |
|
74 * extended. A subclass then can access methods for encoding using specific |
|
75 * representations (e.g. {@link #literal(int, CharSequence, boolean) literal}, |
|
76 * {@link #indexed(int) indexed}, etc.) |
|
77 * |
|
78 * @apiNote |
|
79 * |
|
80 * <p> An Encoder provides an incremental way of encoding headers. |
|
81 * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating |
|
82 * whether, or not, the buffer was sufficiently sized to hold the |
|
83 * remaining of the encoded representation. |
|
84 * |
|
85 * <p> This way, there's no need to provide a buffer of a specific size, or to |
|
86 * resize (and copy) the buffer on demand, when the remaining encoded |
|
87 * representation will not fit in the buffer's remaining space. Instead, an |
|
88 * array of existing buffers can be used, prepended with a frame that encloses |
|
89 * the resulting header block afterwards. |
|
90 * |
|
91 * <p> Splitting the encoding operation into header set up and header encoding, |
|
92 * separates long lived arguments ({@code name}, {@code value}, |
|
93 * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}), |
|
94 * simplifying each operation itself. |
|
95 * |
|
96 * @implNote |
|
97 * |
|
98 * <p> The default implementation does not use dynamic table. It reports to a |
|
99 * coupled Decoder a size update with the value of {@code 0}, and never changes |
|
100 * it afterwards. |
|
101 * |
|
102 * @since 9 |
|
103 */ |
|
104 public class Encoder { |
|
105 |
|
106 private static final AtomicLong ENCODERS_IDS = new AtomicLong(); |
|
107 |
|
108 // TODO: enum: no huffman/smart huffman/always huffman |
|
109 private static final boolean DEFAULT_HUFFMAN = true; |
|
110 |
|
111 private final Logger logger; |
|
112 private final long id; |
|
113 private final IndexedWriter indexedWriter = new IndexedWriter(); |
|
114 private final LiteralWriter literalWriter = new LiteralWriter(); |
|
115 private final LiteralNeverIndexedWriter literalNeverIndexedWriter |
|
116 = new LiteralNeverIndexedWriter(); |
|
117 private final LiteralWithIndexingWriter literalWithIndexingWriter |
|
118 = new LiteralWithIndexingWriter(); |
|
119 private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter(); |
|
120 private final BulkSizeUpdateWriter bulkSizeUpdateWriter |
|
121 = new BulkSizeUpdateWriter(); |
|
122 |
|
123 private BinaryRepresentationWriter writer; |
|
124 private final HeaderTable headerTable; |
|
125 |
|
126 private boolean encoding; |
|
127 |
|
128 private int maxCapacity; |
|
129 private int currCapacity; |
|
130 private int lastCapacity; |
|
131 private long minCapacity; |
|
132 private boolean capacityUpdate; |
|
133 private boolean configuredCapacityUpdate; |
|
134 |
|
135 /** |
|
136 * Constructs an {@code Encoder} with the specified maximum capacity of the |
|
137 * header table. |
|
138 * |
|
139 * <p> The value has to be agreed between decoder and encoder out-of-band, |
|
140 * e.g. by a protocol that uses HPACK |
|
141 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). |
|
142 * |
|
143 * @param maxCapacity |
|
144 * a non-negative integer |
|
145 * |
|
146 * @throws IllegalArgumentException |
|
147 * if maxCapacity is negative |
|
148 */ |
|
149 public Encoder(int maxCapacity) { |
|
150 id = ENCODERS_IDS.incrementAndGet(); |
|
151 this.logger = HPACK.getLogger().subLogger("Encoder#" + id); |
|
152 if (logger.isLoggable(NORMAL)) { |
|
153 logger.log(NORMAL, () -> format("new encoder with maximum table size %s", |
|
154 maxCapacity)); |
|
155 } |
|
156 if (logger.isLoggable(EXTRA)) { |
|
157 /* To correlate with logging outside HPACK, knowing |
|
158 hashCode/toString is important */ |
|
159 logger.log(EXTRA, () -> { |
|
160 String hashCode = Integer.toHexString( |
|
161 System.identityHashCode(this)); |
|
162 /* Since Encoder can be subclassed hashCode AND identity |
|
163 hashCode might be different. So let's print both. */ |
|
164 return format("toString='%s', hashCode=%s, identityHashCode=%s", |
|
165 toString(), hashCode(), hashCode); |
|
166 }); |
|
167 } |
|
168 if (maxCapacity < 0) { |
|
169 throw new IllegalArgumentException( |
|
170 "maxCapacity >= 0: " + maxCapacity); |
|
171 } |
|
172 // Initial maximum capacity update mechanics |
|
173 minCapacity = Long.MAX_VALUE; |
|
174 currCapacity = -1; |
|
175 setMaxCapacity0(maxCapacity); |
|
176 headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable")); |
|
177 } |
|
178 |
|
179 /** |
|
180 * Sets up the given header {@code (name, value)}. |
|
181 * |
|
182 * <p> Fixates {@code name} and {@code value} for the duration of encoding. |
|
183 * |
|
184 * @param name |
|
185 * the name |
|
186 * @param value |
|
187 * the value |
|
188 * |
|
189 * @throws NullPointerException |
|
190 * if any of the arguments are {@code null} |
|
191 * @throws IllegalStateException |
|
192 * if the encoder hasn't fully encoded the previous header, or |
|
193 * hasn't yet started to encode it |
|
194 * @see #header(CharSequence, CharSequence, boolean) |
|
195 */ |
|
196 public void header(CharSequence name, CharSequence value) |
|
197 throws IllegalStateException { |
|
198 header(name, value, false); |
|
199 } |
|
200 |
|
201 /** |
|
202 * Sets up the given header {@code (name, value)} with possibly sensitive |
|
203 * value. |
|
204 * |
|
205 * <p> If the {@code value} is sensitive (think security, secrecy, etc.) |
|
206 * this encoder will compress it using a special representation |
|
207 * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>). |
|
208 * |
|
209 * <p> Fixates {@code name} and {@code value} for the duration of encoding. |
|
210 * |
|
211 * @param name |
|
212 * the name |
|
213 * @param value |
|
214 * the value |
|
215 * @param sensitive |
|
216 * whether or not the value is sensitive |
|
217 * |
|
218 * @throws NullPointerException |
|
219 * if any of the arguments are {@code null} |
|
220 * @throws IllegalStateException |
|
221 * if the encoder hasn't fully encoded the previous header, or |
|
222 * hasn't yet started to encode it |
|
223 * @see #header(CharSequence, CharSequence) |
|
224 * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) |
|
225 */ |
|
226 public void header(CharSequence name, |
|
227 CharSequence value, |
|
228 boolean sensitive) throws IllegalStateException { |
|
229 if (logger.isLoggable(NORMAL)) { |
|
230 logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s", |
|
231 name, value, sensitive)); |
|
232 } |
|
233 // Arguably a good balance between complexity of implementation and |
|
234 // efficiency of encoding |
|
235 requireNonNull(name, "name"); |
|
236 requireNonNull(value, "value"); |
|
237 HeaderTable t = getHeaderTable(); |
|
238 int index = t.indexOf(name, value); |
|
239 if (index > 0) { |
|
240 indexed(index); |
|
241 } else if (index < 0) { |
|
242 if (sensitive) { |
|
243 literalNeverIndexed(-index, value, DEFAULT_HUFFMAN); |
|
244 } else { |
|
245 literal(-index, value, DEFAULT_HUFFMAN); |
|
246 } |
|
247 } else { |
|
248 if (sensitive) { |
|
249 literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); |
|
250 } else { |
|
251 literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); |
|
252 } |
|
253 } |
|
254 } |
|
255 |
|
256 /** |
|
257 * Sets a maximum capacity of the header table. |
|
258 * |
|
259 * <p> The value has to be agreed between decoder and encoder out-of-band, |
|
260 * e.g. by a protocol that uses HPACK |
|
261 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). |
|
262 * |
|
263 * <p> May be called any number of times after or before a complete header |
|
264 * has been encoded. |
|
265 * |
|
266 * <p> If the encoder decides to change the actual capacity, an update will |
|
267 * be encoded before a new encoding operation starts. |
|
268 * |
|
269 * @param capacity |
|
270 * a non-negative integer |
|
271 * |
|
272 * @throws IllegalArgumentException |
|
273 * if capacity is negative |
|
274 * @throws IllegalStateException |
|
275 * if the encoder hasn't fully encoded the previous header, or |
|
276 * hasn't yet started to encode it |
|
277 */ |
|
278 public void setMaxCapacity(int capacity) { |
|
279 if (logger.isLoggable(NORMAL)) { |
|
280 logger.log(NORMAL, () -> format("setting maximum table size to %s", |
|
281 capacity)); |
|
282 } |
|
283 setMaxCapacity0(capacity); |
|
284 } |
|
285 |
|
286 private void setMaxCapacity0(int capacity) { |
|
287 checkEncoding(); |
|
288 if (capacity < 0) { |
|
289 throw new IllegalArgumentException("capacity >= 0: " + capacity); |
|
290 } |
|
291 int calculated = calculateCapacity(capacity); |
|
292 if (logger.isLoggable(NORMAL)) { |
|
293 logger.log(NORMAL, () -> format("actual maximum table size will be %s", |
|
294 calculated)); |
|
295 } |
|
296 if (calculated < 0 || calculated > capacity) { |
|
297 throw new IllegalArgumentException( |
|
298 format("0 <= calculated <= capacity: calculated=%s, capacity=%s", |
|
299 calculated, capacity)); |
|
300 } |
|
301 capacityUpdate = true; |
|
302 // maxCapacity needs to be updated unconditionally, so the encoder |
|
303 // always has the newest one (in case it decides to update it later |
|
304 // unsolicitedly) |
|
305 // Suppose maxCapacity = 4096, and the encoder has decided to use only |
|
306 // 2048. It later can choose anything else from the region [0, 4096]. |
|
307 maxCapacity = capacity; |
|
308 lastCapacity = calculated; |
|
309 minCapacity = Math.min(minCapacity, lastCapacity); |
|
310 } |
|
311 |
|
312 /** |
|
313 * Calculates actual capacity to be used by this encoder in response to |
|
314 * a request to update maximum table size. |
|
315 * |
|
316 * <p> Default implementation does not add anything to the headers table, |
|
317 * hence this method returns {@code 0}. |
|
318 * |
|
319 * <p> It is an error to return a value {@code c}, where {@code c < 0} or |
|
320 * {@code c > maxCapacity}. |
|
321 * |
|
322 * @param maxCapacity |
|
323 * upper bound |
|
324 * |
|
325 * @return actual capacity |
|
326 */ |
|
327 protected int calculateCapacity(int maxCapacity) { |
|
328 return 0; |
|
329 } |
|
330 |
|
331 /** |
|
332 * Encodes the {@linkplain #header(CharSequence, CharSequence) set up} |
|
333 * header into the given buffer. |
|
334 * |
|
335 * <p> The encoder writes as much as possible of the header's binary |
|
336 * representation into the given buffer, starting at the buffer's position, |
|
337 * and increments its position to reflect the bytes written. The buffer's |
|
338 * mark and limit will not be modified. |
|
339 * |
|
340 * <p> Once the method has returned {@code true}, the current header is |
|
341 * deemed encoded. A new header may be set up. |
|
342 * |
|
343 * @param headerBlock |
|
344 * the buffer to encode the header into, may be empty |
|
345 * |
|
346 * @return {@code true} if the current header has been fully encoded, |
|
347 * {@code false} otherwise |
|
348 * |
|
349 * @throws NullPointerException |
|
350 * if the buffer is {@code null} |
|
351 * @throws ReadOnlyBufferException |
|
352 * if this buffer is read-only |
|
353 * @throws IllegalStateException |
|
354 * if there is no set up header |
|
355 */ |
|
356 public final boolean encode(ByteBuffer headerBlock) { |
|
357 if (!encoding) { |
|
358 throw new IllegalStateException("A header hasn't been set up"); |
|
359 } |
|
360 if (logger.isLoggable(EXTRA)) { |
|
361 logger.log(EXTRA, () -> format("writing to %s", headerBlock)); |
|
362 } |
|
363 if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log |
|
364 return false; |
|
365 } |
|
366 boolean done = writer.write(headerTable, headerBlock); |
|
367 if (done) { |
|
368 writer.reset(); // FIXME: WHY? |
|
369 encoding = false; |
|
370 } |
|
371 return done; |
|
372 } |
|
373 |
|
374 private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) { |
|
375 if (capacityUpdate) { |
|
376 if (!configuredCapacityUpdate) { |
|
377 List<Integer> sizes = new LinkedList<>(); |
|
378 if (minCapacity < currCapacity) { |
|
379 sizes.add((int) minCapacity); |
|
380 if (minCapacity != lastCapacity) { |
|
381 sizes.add(lastCapacity); |
|
382 } |
|
383 } else if (lastCapacity != currCapacity) { |
|
384 sizes.add(lastCapacity); |
|
385 } |
|
386 bulkSizeUpdateWriter.maxHeaderTableSizes(sizes); |
|
387 configuredCapacityUpdate = true; |
|
388 } |
|
389 boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock); |
|
390 if (done) { |
|
391 minCapacity = lastCapacity; |
|
392 currCapacity = lastCapacity; |
|
393 bulkSizeUpdateWriter.reset(); |
|
394 capacityUpdate = false; |
|
395 configuredCapacityUpdate = false; |
|
396 } |
|
397 return done; |
|
398 } |
|
399 return true; |
|
400 } |
|
401 |
|
402 protected final void indexed(int index) throws IndexOutOfBoundsException { |
|
403 checkEncoding(); |
|
404 if (logger.isLoggable(EXTRA)) { |
|
405 logger.log(EXTRA, () -> format("indexed %s", index)); |
|
406 } |
|
407 encoding = true; |
|
408 writer = indexedWriter.index(index); |
|
409 } |
|
410 |
|
411 protected final void literal(int index, |
|
412 CharSequence value, |
|
413 boolean useHuffman) |
|
414 throws IndexOutOfBoundsException { |
|
415 if (logger.isLoggable(EXTRA)) { |
|
416 logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", |
|
417 index, value)); |
|
418 } |
|
419 checkEncoding(); |
|
420 encoding = true; |
|
421 writer = literalWriter |
|
422 .index(index).value(value, useHuffman); |
|
423 } |
|
424 |
|
425 protected final void literal(CharSequence name, |
|
426 boolean nameHuffman, |
|
427 CharSequence value, |
|
428 boolean valueHuffman) { |
|
429 if (logger.isLoggable(EXTRA)) { |
|
430 logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')", |
|
431 name, value)); |
|
432 } |
|
433 checkEncoding(); |
|
434 encoding = true; |
|
435 writer = literalWriter |
|
436 .name(name, nameHuffman).value(value, valueHuffman); |
|
437 } |
|
438 |
|
439 protected final void literalNeverIndexed(int index, |
|
440 CharSequence value, |
|
441 boolean valueHuffman) |
|
442 throws IndexOutOfBoundsException { |
|
443 if (logger.isLoggable(EXTRA)) { |
|
444 logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", |
|
445 index, value)); |
|
446 } |
|
447 checkEncoding(); |
|
448 encoding = true; |
|
449 writer = literalNeverIndexedWriter |
|
450 .index(index).value(value, valueHuffman); |
|
451 } |
|
452 |
|
453 protected final void literalNeverIndexed(CharSequence name, |
|
454 boolean nameHuffman, |
|
455 CharSequence value, |
|
456 boolean valueHuffman) { |
|
457 if (logger.isLoggable(EXTRA)) { |
|
458 logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')", |
|
459 name, value)); |
|
460 } |
|
461 checkEncoding(); |
|
462 encoding = true; |
|
463 writer = literalNeverIndexedWriter |
|
464 .name(name, nameHuffman).value(value, valueHuffman); |
|
465 } |
|
466 |
|
467 protected final void literalWithIndexing(int index, |
|
468 CharSequence value, |
|
469 boolean valueHuffman) |
|
470 throws IndexOutOfBoundsException { |
|
471 if (logger.isLoggable(EXTRA)) { |
|
472 logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", |
|
473 index, value)); |
|
474 } |
|
475 checkEncoding(); |
|
476 encoding = true; |
|
477 writer = literalWithIndexingWriter |
|
478 .index(index).value(value, valueHuffman); |
|
479 } |
|
480 |
|
481 protected final void literalWithIndexing(CharSequence name, |
|
482 boolean nameHuffman, |
|
483 CharSequence value, |
|
484 boolean valueHuffman) { |
|
485 if (logger.isLoggable(EXTRA)) { // TODO: include huffman info? |
|
486 logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')", |
|
487 name, value)); |
|
488 } |
|
489 checkEncoding(); |
|
490 encoding = true; |
|
491 writer = literalWithIndexingWriter |
|
492 .name(name, nameHuffman).value(value, valueHuffman); |
|
493 } |
|
494 |
|
495 protected final void sizeUpdate(int capacity) |
|
496 throws IllegalArgumentException { |
|
497 if (logger.isLoggable(EXTRA)) { |
|
498 logger.log(EXTRA, () -> format("dynamic table size update %s", |
|
499 capacity)); |
|
500 } |
|
501 checkEncoding(); |
|
502 // Ensure subclass follows the contract |
|
503 if (capacity > this.maxCapacity) { |
|
504 throw new IllegalArgumentException( |
|
505 format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s", |
|
506 capacity, maxCapacity)); |
|
507 } |
|
508 writer = sizeUpdateWriter.maxHeaderTableSize(capacity); |
|
509 } |
|
510 |
|
511 protected final int getMaxCapacity() { |
|
512 return maxCapacity; |
|
513 } |
|
514 |
|
515 protected final HeaderTable getHeaderTable() { |
|
516 return headerTable; |
|
517 } |
|
518 |
|
519 protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress() |
|
520 if (encoding) { |
|
521 throw new IllegalStateException( |
|
522 "Previous encoding operation hasn't finished yet"); |
|
523 } |
|
524 } |
|
525 } |