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 import jdk.internal.vm.annotation.Stable; 29 30 import java.io.IOException; 31 import java.nio.ByteBuffer; 32 import java.util.concurrent.atomic.AtomicLong; 33 34 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA; 35 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL; 36 import static java.lang.String.format; 37 import static java.util.Objects.requireNonNull; 38 39 /** 40 * Decodes headers from their binary representation. 41 * 42 * <p> Typical lifecycle looks like this: 43 * 44 * <p> {@link #Decoder(int) new Decoder} 45 * ({@link #setMaxCapacity(int) setMaxCapacity}? 46 * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* 47 * 48 * @apiNote 49 * 50 * <p> The design intentions behind Decoder were to facilitate flexible and 51 * incremental style of processing. 52 * 53 * <p> {@code Decoder} does not require a complete header block in a single 54 * {@code ByteBuffer}. The header block can be spread across many buffers of any 55 * size and decoded one-by-one the way it makes most sense for the user. This 56 * way also allows not to limit the size of the header block. 57 * 58 * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as 59 * soon as they become decoded. Using the callback also gives the user a freedom 60 * to decide how headers are processed. The callback does not limit the number 61 * of headers decoded during single decoding operation. 62 * 63 * @since 9 64 */ 65 public final class Decoder { 66 67 private final Logger logger; 68 private static final AtomicLong DECODERS_IDS = new AtomicLong(); 69 70 @Stable 71 private static final State[] states = new State[256]; 72 73 static { 74 // To be able to do a quick lookup, each of 256 possibilities are mapped 75 // to corresponding states. 76 // 77 // We can safely do this since patterns 1, 01, 001, 0001, 0000 are 78 // Huffman prefixes and therefore are inherently not ambiguous. 79 // 80 // I do it mainly for better debugging (to not go each time step by step 81 // through if...else tree). As for performance win for the decoding, I 82 // believe is negligible. 83 for (int i = 0; i < states.length; i++) { 84 if ((i & 0b1000_0000) == 0b1000_0000) { 85 states[i] = State.INDEXED; 86 } else if ((i & 0b1100_0000) == 0b0100_0000) { 87 states[i] = State.LITERAL_WITH_INDEXING; 88 } else if ((i & 0b1110_0000) == 0b0010_0000) { 89 states[i] = State.SIZE_UPDATE; 90 } else if ((i & 0b1111_0000) == 0b0001_0000) { 91 states[i] = State.LITERAL_NEVER_INDEXED; 92 } else if ((i & 0b1111_0000) == 0b0000_0000) { 93 states[i] = State.LITERAL; 94 } else { 95 throw new InternalError(String.valueOf(i)); 96 } 97 } 98 } 99 100 private final long id; 101 private final HeaderTable table; 102 103 private State state = State.READY; 104 private final IntegerReader integerReader; 105 private final StringReader stringReader; 106 private final StringBuilder name; 107 private final StringBuilder value; 108 private int intValue; 109 private boolean firstValueRead; 110 private boolean firstValueIndex; 111 private boolean nameHuffmanEncoded; 112 private boolean valueHuffmanEncoded; 113 private int capacity; 114 115 /** 116 * Constructs a {@code Decoder} with the specified initial capacity of the 117 * header table. 118 * 119 * <p> The value has to be agreed between decoder and encoder out-of-band, 120 * e.g. by a protocol that uses HPACK 121 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). 122 * 123 * @param capacity 124 * a non-negative integer 125 * 126 * @throws IllegalArgumentException 127 * if capacity is negative 128 */ 129 public Decoder(int capacity) { 130 id = DECODERS_IDS.incrementAndGet(); 131 logger = HPACK.getLogger().subLogger("Decoder#" + id); 132 if (logger.isLoggable(NORMAL)) { 133 logger.log(NORMAL, () -> format("new decoder with maximum table size %s", 134 capacity)); 135 } 136 if (logger.isLoggable(NORMAL)) { 137 /* To correlate with logging outside HPACK, knowing 138 hashCode/toString is important */ 139 logger.log(NORMAL, () -> { 140 String hashCode = Integer.toHexString( 141 System.identityHashCode(this)); 142 return format("toString='%s', identityHashCode=%s", 143 toString(), hashCode); 144 }); 145 } 146 setMaxCapacity0(capacity); 147 table = new HeaderTable(capacity, logger.subLogger("HeaderTable")); 148 integerReader = new IntegerReader(); 149 stringReader = new StringReader(); 150 name = new StringBuilder(512); 151 value = new StringBuilder(1024); 152 } 153 154 /** 155 * Sets a maximum capacity of the header table. 156 * 157 * <p> The value has to be agreed between decoder and encoder out-of-band, 158 * e.g. by a protocol that uses HPACK 159 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). 160 * 161 * @param capacity 162 * a non-negative integer 163 * 164 * @throws IllegalArgumentException 165 * if capacity is negative 166 */ 167 public void setMaxCapacity(int capacity) { 168 if (logger.isLoggable(NORMAL)) { 169 logger.log(NORMAL, () -> format("setting maximum table size to %s", 170 capacity)); 171 } 172 setMaxCapacity0(capacity); 173 } 174 175 private void setMaxCapacity0(int capacity) { 176 if (capacity < 0) { 177 throw new IllegalArgumentException("capacity >= 0: " + capacity); 178 } 179 // FIXME: await capacity update if less than what was prior to it 180 this.capacity = capacity; 181 } 182 183 /** 184 * Decodes a header block from the given buffer to the given callback. 185 * 186 * <p> Suppose a header block is represented by a sequence of 187 * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the 188 * consumer of decoded headers is represented by the callback. Then to 189 * decode the header block, the following approach might be used: 190 * 191 * <pre>{@code 192 * while (buffers.hasNext()) { 193 * ByteBuffer input = buffers.next(); 194 * decoder.decode(input, callback, !buffers.hasNext()); 195 * } 196 * }</pre> 197 * 198 * <p> The decoder reads as much as possible of the header block from the 199 * given buffer, starting at the buffer's position, and increments its 200 * position to reflect the bytes read. The buffer's mark and limit will not 201 * be modified. 202 * 203 * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the 204 * current header block is deemed ended, and inconsistencies, if any, are 205 * reported immediately by throwing an {@code IOException}. 206 * 207 * <p> Each callback method is called only after the implementation has 208 * processed the corresponding bytes. If the bytes revealed a decoding 209 * error, the callback method is not called. 210 * 211 * <p> In addition to exceptions thrown directly by the method, any 212 * exceptions thrown from the {@code callback} will bubble up. 213 * 214 * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of 215 * returning it for two reasons. The first one is that the user of the 216 * decoder always knows which chunk is the last. The second one is to throw 217 * the most detailed exception possible, which might be useful for 218 * diagnosing issues. 219 * 220 * @implNote This implementation is not atomic in respect to decoding 221 * errors. In other words, if the decoding operation has thrown a decoding 222 * error, the decoder is no longer usable. 223 * 224 * @param headerBlock 225 * the chunk of the header block, may be empty 226 * @param endOfHeaderBlock 227 * true if the chunk is the final (or the only one) in the sequence 228 * 229 * @param consumer 230 * the callback 231 * @throws IOException 232 * in case of a decoding error 233 * @throws NullPointerException 234 * if either headerBlock or consumer are null 235 */ 236 public void decode(ByteBuffer headerBlock, 237 boolean endOfHeaderBlock, 238 DecodingCallback consumer) throws IOException { 239 requireNonNull(headerBlock, "headerBlock"); 240 requireNonNull(consumer, "consumer"); 241 if (logger.isLoggable(NORMAL)) { 242 logger.log(NORMAL, () -> format("reading %s, end of header block? %s", 243 headerBlock, endOfHeaderBlock)); 244 } 245 while (headerBlock.hasRemaining()) { 246 proceed(headerBlock, consumer); 247 } 248 if (endOfHeaderBlock && state != State.READY) { 249 logger.log(NORMAL, () -> format("unexpected end of %s representation", 250 state)); 251 throw new IOException("Unexpected end of header block"); 252 } 253 } 254 255 private void proceed(ByteBuffer input, DecodingCallback action) 256 throws IOException { 257 switch (state) { 258 case READY: 259 resumeReady(input); 260 break; 261 case INDEXED: 262 resumeIndexed(input, action); 263 break; 264 case LITERAL: 265 resumeLiteral(input, action); 266 break; 267 case LITERAL_WITH_INDEXING: 268 resumeLiteralWithIndexing(input, action); 269 break; 270 case LITERAL_NEVER_INDEXED: 271 resumeLiteralNeverIndexed(input, action); 272 break; 273 case SIZE_UPDATE: 274 resumeSizeUpdate(input, action); 275 break; 276 default: 277 throw new InternalError("Unexpected decoder state: " + state); 278 } 279 } 280 281 private void resumeReady(ByteBuffer input) { 282 int b = input.get(input.position()) & 0xff; // absolute read 283 State s = states[b]; 284 if (logger.isLoggable(EXTRA)) { 285 logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)", 286 s, b)); 287 } 288 switch (s) { 289 case INDEXED: 290 integerReader.configure(7); 291 state = State.INDEXED; 292 firstValueIndex = true; 293 break; 294 case LITERAL: 295 state = State.LITERAL; 296 firstValueIndex = (b & 0b0000_1111) != 0; 297 if (firstValueIndex) { 298 integerReader.configure(4); 299 } 300 break; 301 case LITERAL_WITH_INDEXING: 302 state = State.LITERAL_WITH_INDEXING; 303 firstValueIndex = (b & 0b0011_1111) != 0; 304 if (firstValueIndex) { 305 integerReader.configure(6); 306 } 307 break; 308 case LITERAL_NEVER_INDEXED: 309 state = State.LITERAL_NEVER_INDEXED; 310 firstValueIndex = (b & 0b0000_1111) != 0; 311 if (firstValueIndex) { 312 integerReader.configure(4); 313 } 314 break; 315 case SIZE_UPDATE: 316 integerReader.configure(5); 317 state = State.SIZE_UPDATE; 318 firstValueIndex = true; 319 break; 320 default: 321 throw new InternalError(String.valueOf(s)); 322 } 323 if (!firstValueIndex) { 324 input.get(); // advance, next stop: "String Literal" 325 } 326 } 327 328 // 0 1 2 3 4 5 6 7 329 // +---+---+---+---+---+---+---+---+ 330 // | 1 | Index (7+) | 331 // +---+---------------------------+ 332 // 333 private void resumeIndexed(ByteBuffer input, DecodingCallback action) 334 throws IOException { 335 if (!integerReader.read(input)) { 336 return; 337 } 338 intValue = integerReader.get(); 339 integerReader.reset(); 340 if (logger.isLoggable(NORMAL)) { 341 logger.log(NORMAL, () -> format("indexed %s", intValue)); 342 } 343 try { 344 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 345 action.onIndexed(intValue, f.name, f.value); 346 } finally { 347 state = State.READY; 348 } 349 } 350 351 private HeaderTable.HeaderField getHeaderFieldAt(int index) 352 throws IOException 353 { 354 HeaderTable.HeaderField f; 355 try { 356 f = table.get(index); 357 } catch (IndexOutOfBoundsException e) { 358 throw new IOException("header fields table index", e); 359 } 360 return f; 361 } 362 363 // 0 1 2 3 4 5 6 7 364 // +---+---+---+---+---+---+---+---+ 365 // | 0 | 0 | 0 | 0 | Index (4+) | 366 // +---+---+-----------------------+ 367 // | H | Value Length (7+) | 368 // +---+---------------------------+ 369 // | Value String (Length octets) | 370 // +-------------------------------+ 371 // 372 // 0 1 2 3 4 5 6 7 373 // +---+---+---+---+---+---+---+---+ 374 // | 0 | 0 | 0 | 0 | 0 | 375 // +---+---+-----------------------+ 376 // | H | Name Length (7+) | 377 // +---+---------------------------+ 378 // | Name String (Length octets) | 379 // +---+---------------------------+ 380 // | H | Value Length (7+) | 381 // +---+---------------------------+ 382 // | Value String (Length octets) | 383 // +-------------------------------+ 384 // 385 private void resumeLiteral(ByteBuffer input, DecodingCallback action) 386 throws IOException { 387 if (!completeReading(input)) { 388 return; 389 } 390 try { 391 if (firstValueIndex) { 392 if (logger.isLoggable(NORMAL)) { 393 logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", 394 intValue, value)); 395 } 396 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 397 action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); 398 } else { 399 if (logger.isLoggable(NORMAL)) { 400 logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", 401 name, value)); 402 } 403 action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 404 } 405 } finally { 406 cleanUpAfterReading(); 407 } 408 } 409 410 // 411 // 0 1 2 3 4 5 6 7 412 // +---+---+---+---+---+---+---+---+ 413 // | 0 | 1 | Index (6+) | 414 // +---+---+-----------------------+ 415 // | H | Value Length (7+) | 416 // +---+---------------------------+ 417 // | Value String (Length octets) | 418 // +-------------------------------+ 419 // 420 // 0 1 2 3 4 5 6 7 421 // +---+---+---+---+---+---+---+---+ 422 // | 0 | 1 | 0 | 423 // +---+---+-----------------------+ 424 // | H | Name Length (7+) | 425 // +---+---------------------------+ 426 // | Name String (Length octets) | 427 // +---+---------------------------+ 428 // | H | Value Length (7+) | 429 // +---+---------------------------+ 430 // | Value String (Length octets) | 431 // +-------------------------------+ 432 // 433 private void resumeLiteralWithIndexing(ByteBuffer input, 434 DecodingCallback action) 435 throws IOException { 436 if (!completeReading(input)) { 437 return; 438 } 439 try { 440 // 441 // 1. (name, value) will be stored in the table as strings 442 // 2. Most likely the callback will also create strings from them 443 // ------------------------------------------------------------------------ 444 // Let's create those string beforehand (and only once!) to benefit everyone 445 // 446 String n; 447 String v = value.toString(); 448 if (firstValueIndex) { 449 if (logger.isLoggable(NORMAL)) { 450 logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", 451 intValue, value)); 452 } 453 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 454 n = f.name; 455 action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); 456 } else { 457 n = name.toString(); 458 if (logger.isLoggable(NORMAL)) { 459 logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", 460 n, value)); 461 } 462 action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); 463 } 464 table.put(n, v); 465 } finally { 466 cleanUpAfterReading(); 467 } 468 } 469 470 // 0 1 2 3 4 5 6 7 471 // +---+---+---+---+---+---+---+---+ 472 // | 0 | 0 | 0 | 1 | Index (4+) | 473 // +---+---+-----------------------+ 474 // | H | Value Length (7+) | 475 // +---+---------------------------+ 476 // | Value String (Length octets) | 477 // +-------------------------------+ 478 // 479 // 0 1 2 3 4 5 6 7 480 // +---+---+---+---+---+---+---+---+ 481 // | 0 | 0 | 0 | 1 | 0 | 482 // +---+---+-----------------------+ 483 // | H | Name Length (7+) | 484 // +---+---------------------------+ 485 // | Name String (Length octets) | 486 // +---+---------------------------+ 487 // | H | Value Length (7+) | 488 // +---+---------------------------+ 489 // | Value String (Length octets) | 490 // +-------------------------------+ 491 // 492 private void resumeLiteralNeverIndexed(ByteBuffer input, 493 DecodingCallback action) 494 throws IOException { 495 if (!completeReading(input)) { 496 return; 497 } 498 try { 499 if (firstValueIndex) { 500 if (logger.isLoggable(NORMAL)) { 501 logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", 502 intValue, value)); 503 } 504 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 505 action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); 506 } else { 507 if (logger.isLoggable(NORMAL)) { 508 logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", 509 name, value)); 510 } 511 action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 512 } 513 } finally { 514 cleanUpAfterReading(); 515 } 516 } 517 518 // 0 1 2 3 4 5 6 7 519 // +---+---+---+---+---+---+---+---+ 520 // | 0 | 0 | 1 | Max size (5+) | 521 // +---+---------------------------+ 522 // 523 private void resumeSizeUpdate(ByteBuffer input, 524 DecodingCallback action) throws IOException { 525 if (!integerReader.read(input)) { 526 return; 527 } 528 intValue = integerReader.get(); 529 if (logger.isLoggable(NORMAL)) { 530 logger.log(NORMAL, () -> format("dynamic table size update %s", 531 intValue)); 532 } 533 assert intValue >= 0; 534 if (intValue > capacity) { 535 throw new IOException( 536 format("Received capacity exceeds expected: capacity=%s, expected=%s", 537 intValue, capacity)); 538 } 539 integerReader.reset(); 540 try { 541 action.onSizeUpdate(intValue); 542 table.setMaxSize(intValue); 543 } finally { 544 state = State.READY; 545 } 546 } 547 548 private boolean completeReading(ByteBuffer input) throws IOException { 549 if (!firstValueRead) { 550 if (firstValueIndex) { 551 if (!integerReader.read(input)) { 552 return false; 553 } 554 intValue = integerReader.get(); 555 integerReader.reset(); 556 } else { 557 if (!stringReader.read(input, name)) { 558 return false; 559 } 560 nameHuffmanEncoded = stringReader.isHuffmanEncoded(); 561 stringReader.reset(); 562 } 563 firstValueRead = true; 564 return false; 565 } else { 566 if (!stringReader.read(input, value)) { 567 return false; 568 } 569 } 570 valueHuffmanEncoded = stringReader.isHuffmanEncoded(); 571 stringReader.reset(); 572 return true; 573 } 574 575 private void cleanUpAfterReading() { 576 name.setLength(0); 577 value.setLength(0); 578 firstValueRead = false; 579 state = State.READY; 580 } 581 582 private enum State { 583 READY, 584 INDEXED, 585 LITERAL_NEVER_INDEXED, 586 LITERAL, 587 LITERAL_WITH_INDEXING, 588 SIZE_UPDATE 589 } 590 591 HeaderTable getTable() { 592 return table; 593 } 594 }