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 }