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. 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.incubator.http.internal.hpack; 26 27 import jdk.incubator.http.internal.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.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA; 38 import static jdk.incubator.http.internal.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 }