diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java index 8aebd005f8b3..720faf4dd075 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java @@ -188,5 +188,49 @@ else if (b >= 0x80) // OBS } } } + + /** + * This is used when decoding to not decode illegal characters based on RFC9110. + * CR, LF, or NUL are replaced with ' ', all other control and multibyte characters + * are replaced with '?'. If this is given a legal character the same value will be returned. + *
+     * field-vchar = VCHAR / obs-text
+     * obs-text    = %x80-FF
+     * VCHAR       = %x21-7E
+     * 
+ * @param c the character to test. + * @return the original character or the replacement character ' ' or '?', + * the return value is guaranteed to be a valid ISO-8859-1 character. + */ + public static char sanitizeFieldVchar(char c) + { + switch (c) + { + // A recipient of CR, LF, or NUL within a field value MUST either reject the message + // or replace each of those characters with SP before further processing + case '\r': + case '\n': + case 0x00: + return ' '; + + default: + if (isIllegalFieldVchar(c)) + return '?'; + } + return c; + } + + /** + * Checks whether this is an invalid VCHAR based on RFC9110. + * If this not a valid ISO-8859-1 character or a control character + * we say that it is illegal. + * + * @param c the character to test. + * @return true if this is invalid VCHAR. + */ + public static boolean isIllegalFieldVchar(char c) + { + return (c >= 256 || c < ' '); + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java new file mode 100644 index 000000000000..5f0555765367 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java @@ -0,0 +1,27 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +public class EncodingException extends Exception +{ + public EncodingException(String message) + { + super(message); + } +} diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java similarity index 82% rename from jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java index 26e948b6644e..6e2cb33c21e6 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java @@ -16,14 +16,16 @@ // ======================================================================== // -package org.eclipse.jetty.http2.hpack; - -import java.nio.ByteBuffer; - -import org.eclipse.jetty.util.Utf8StringBuilder; +package org.eclipse.jetty.http.compression; +/** + * This class contains the Huffman Codes defined in RFC7541. + */ public class Huffman { + private Huffman() + { + } // Appendix C: Huffman Codes // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C @@ -291,7 +293,7 @@ public class Huffman static final int[][] LCCODES = new int[CODES.length][]; static final char EOS = 256; - // Huffman decode tree stored in a flattened char array for good + // Huffman decode tree stored in a flattened char array for good // locality of reference. static final char[] tree; static final char[] rowsym; @@ -307,9 +309,9 @@ public class Huffman } int r = 0; - for (int i = 0; i < CODES.length; i++) + for (int[] ints : CODES) { - r += (CODES[i][1] + 7) / 8; + r += (ints[1] + 7) / 8; } tree = new char[r * 256]; rowsym = new char[r]; @@ -352,200 +354,4 @@ public class Huffman } } } - - public static String decode(ByteBuffer buffer) throws HpackException.CompressionException - { - return decode(buffer, buffer.remaining()); - } - - public static String decode(ByteBuffer buffer, int length) throws HpackException.CompressionException - { - Utf8StringBuilder utf8 = new Utf8StringBuilder(length * 2); - int node = 0; - int current = 0; - int bits = 0; - - for (int i = 0; i < length; i++) - { - int b = buffer.get() & 0xFF; - current = (current << 8) | b; - bits += 8; - while (bits >= 8) - { - int c = (current >>> (bits - 8)) & 0xFF; - node = tree[node * 256 + c]; - if (rowbits[node] != 0) - { - if (rowsym[node] == EOS) - throw new HpackException.CompressionException("EOS in content"); - - // terminal node - utf8.append((byte)(0xFF & rowsym[node])); - bits -= rowbits[node]; - node = 0; - } - else - { - // non-terminal node - bits -= 8; - } - } - } - - while (bits > 0) - { - int c = (current << (8 - bits)) & 0xFF; - int lastNode = node; - node = tree[node * 256 + c]; - - if (rowbits[node] == 0 || rowbits[node] > bits) - { - int requiredPadding = 0; - for (int i = 0; i < bits; i++) - { - requiredPadding = (requiredPadding << 1) | 1; - } - - if ((c >> (8 - bits)) != requiredPadding) - throw new HpackException.CompressionException("Incorrect padding"); - - node = lastNode; - break; - } - - utf8.append((byte)(0xFF & rowsym[node])); - bits -= rowbits[node]; - node = 0; - } - - if (node != 0) - throw new HpackException.CompressionException("Bad termination"); - - return utf8.toString(); - } - - public static int octetsNeeded(String s) - { - return octetsNeeded(CODES, s); - } - - public static int octetsNeeded(byte[] b) - { - return octetsNeeded(CODES, b); - } - - public static void encode(ByteBuffer buffer, String s) - { - encode(CODES, buffer, s); - } - - public static void encode(ByteBuffer buffer, byte[] b) - { - encode(CODES, buffer, b); - } - - public static int octetsNeededLC(String s) - { - return octetsNeeded(LCCODES, s); - } - - public static void encodeLC(ByteBuffer buffer, String s) - { - encode(LCCODES, buffer, s); - } - - private static int octetsNeeded(final int[][] table, String s) - { - int needed = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - return -1; - needed += table[c][1]; - } - - return (needed + 7) / 8; - } - - private static int octetsNeeded(final int[][] table, byte[] b) - { - int needed = 0; - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - needed += table[c][1]; - } - return (needed + 7) / 8; - } - - /** - * @param table The table to encode by - * @param buffer The buffer to encode to - * @param s The string to encode - */ - private static void encode(final int[][] table, ByteBuffer buffer, String s) - { - long current = 0; - int n = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - throw new IllegalArgumentException(); - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } - - private static void encode(final int[][] table, ByteBuffer buffer, byte[] b) - { - long current = 0; - int n = 0; - - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java new file mode 100644 index 000000000000..48851c113f11 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java @@ -0,0 +1,143 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http.HttpTokens; +import org.eclipse.jetty.util.CharsetStringBuilder; + +import static org.eclipse.jetty.http.compression.Huffman.rowbits; +import static org.eclipse.jetty.http.compression.Huffman.rowsym; + +/** + *

Used to decoded Huffman encoded strings.

+ * + *

Characters which are illegal field-vchar values are replaced with + * either ' ' or '?' as described in RFC9110

+ */ +public class HuffmanDecoder +{ + private final CharsetStringBuilder.Iso88591StringBuilder _builder = new CharsetStringBuilder.Iso88591StringBuilder(); + private int _length = 0; + private int _count = 0; + private int _node = 0; + private int _current = 0; + private int _bits = 0; + + /** + * @param length in bytes of the huffman data. + */ + public void setLength(int length) + { + if (_count != 0) + throw new IllegalStateException(); + _length = length; + } + + /** + * @param buffer the buffer containing the Huffman encoded bytes. + * @return the decoded String. + * @throws EncodingException if the huffman encoding is invalid. + */ + public String decode(ByteBuffer buffer) throws EncodingException + { + for (; _count < _length; _count++) + { + if (!buffer.hasRemaining()) + return null; + + int b = buffer.get() & 0xFF; + _current = (_current << 8) | b; + _bits += 8; + while (_bits >= 8) + { + int i = (_current >>> (_bits - 8)) & 0xFF; + _node = Huffman.tree[_node * 256 + i]; + if (rowbits[_node] != 0) + { + if (rowsym[_node] == Huffman.EOS) + { + reset(); + throw new EncodingException("eos_in_content"); + } + + // terminal node + char c = rowsym[_node]; + c = HttpTokens.sanitizeFieldVchar(c); + _builder.append((byte)c); + _bits -= rowbits[_node]; + _node = 0; + } + else + { + // non-terminal node + _bits -= 8; + } + } + } + + while (_bits > 0) + { + int i = (_current << (8 - _bits)) & 0xFF; + int lastNode = _node; + _node = Huffman.tree[_node * 256 + i]; + + if (rowbits[_node] == 0 || rowbits[_node] > _bits) + { + int requiredPadding = 0; + for (int j = 0; j < _bits; j++) + { + requiredPadding = (requiredPadding << 1) | 1; + } + + if ((i >> (8 - _bits)) != requiredPadding) + throw new EncodingException("incorrect_padding"); + + _node = lastNode; + break; + } + + char c = rowsym[_node]; + c = HttpTokens.sanitizeFieldVchar(c); + _builder.append((byte)c); + _bits -= rowbits[_node]; + _node = 0; + } + + if (_node != 0) + { + reset(); + throw new EncodingException("bad_termination"); + } + + String value = _builder.build(); + reset(); + return value; + } + + public void reset() + { + _builder.reset(); + _count = 0; + _current = 0; + _node = 0; + _bits = 0; + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java new file mode 100644 index 000000000000..533fc21dca8b --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java @@ -0,0 +1,142 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http.HttpTokens; + +import static org.eclipse.jetty.http.compression.Huffman.CODES; +import static org.eclipse.jetty.http.compression.Huffman.LCCODES; + +/** + *

Used to encode strings Huffman encoding.

+ * + *

Characters are encoded with ISO-8859-1, if any multi-byte characters or + * control characters are present the encoder will throw {@link EncodingException}.

+ */ +public class HuffmanEncoder +{ + private HuffmanEncoder() + { + } + + /** + * @param s the string to encode. + * @return the number of octets needed to encode the string, or -1 if it cannot be encoded. + */ + public static int octetsNeeded(String s) + { + return octetsNeeded(CODES, s); + } + + /** + * @param b the byte array to encode. + * @return the number of octets needed to encode the bytes, or -1 if it cannot be encoded. + */ + public static int octetsNeeded(byte[] b) + { + int needed = 0; + for (byte value : b) + { + int c = 0xFF & value; + needed += CODES[c][1]; + } + return (needed + 7) / 8; + } + + /** + * @param buffer the buffer to encode into. + * @param s the string to encode. + */ + public static void encode(ByteBuffer buffer, String s) + { + encode(CODES, buffer, s); + } + + /** + * @param s the string to encode in lowercase. + * @return the number of octets needed to encode the string, or -1 if it cannot be encoded. + */ + public static int octetsNeededLowerCase(String s) + { + return octetsNeeded(LCCODES, s); + } + + /** + * @param buffer the buffer to encode into in lowercase. + * @param s the string to encode. + */ + public static void encodeLowerCase(ByteBuffer buffer, String s) + { + encode(LCCODES, buffer, s); + } + + private static int octetsNeeded(final int[][] table, String s) + { + int needed = 0; + int len = s.length(); + for (int i = 0; i < len; i++) + { + char c = s.charAt(i); + if (HttpTokens.isIllegalFieldVchar(c)) + return -1; + needed += table[c][1]; + } + + return (needed + 7) / 8; + } + + /** + * @param table The table to encode by + * @param buffer The buffer to encode to + * @param s The string to encode + */ + private static void encode(final int[][] table, ByteBuffer buffer, String s) + { + long current = 0; + int n = 0; + int len = s.length(); + for (int i = 0; i < len; i++) + { + char c = s.charAt(i); + if (HttpTokens.isIllegalFieldVchar(c)) + throw new IllegalArgumentException(); + int code = table[c][0]; + int bits = table[c][1]; + + current <<= bits; + current |= code; + n += bits; + + while (n >= 8) + { + n -= 8; + buffer.put((byte)(current >> n)); + } + } + + if (n > 0) + { + current <<= (8 - n); + current |= (0xFF >>> n); + buffer.put((byte)(current)); + } + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java new file mode 100644 index 000000000000..590b8c195a22 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java @@ -0,0 +1,113 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +/** + * Used to decode integers as described in RFC7541. + */ +public class NBitIntegerDecoder +{ + private int _prefix; + private long _total; + private long _multiplier; + private boolean _started; + + /** + * Set the prefix length in of the integer representation in bits. + * A prefix of 6 means the integer representation starts after the first 2 bits. + * @param prefix the number of bits in the integer prefix. + */ + public void setPrefix(int prefix) + { + if (_started) + throw new IllegalStateException(); + _prefix = prefix; + } + + /** + * Decode an integer from the buffer. If the buffer does not contain the complete integer representation + * a value of -1 is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded integer. + * @return the decoded integer or -1 to indicate that more data is needed. + * @throws ArithmeticException if the value overflows a int. + */ + public int decodeInt(ByteBuffer buffer) + { + return Math.toIntExact(decodeLong(buffer)); + } + + /** + * Decode a long from the buffer. If the buffer does not contain the complete integer representation + * a value of -1 is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded integer. + * @return the decoded long or -1 to indicate that more data is needed. + * @throws ArithmeticException if the value overflows a long. + */ + public long decodeLong(ByteBuffer buffer) + { + if (!_started) + { + if (!buffer.hasRemaining()) + return -1; + + _started = true; + _multiplier = 1; + int nbits = 0xFF >>> (8 - _prefix); + _total = buffer.get() & nbits; + if (_total < nbits) + { + long total = _total; + reset(); + return total; + } + } + + while (true) + { + // If we have no more remaining we return -1 to indicate that more data is needed to continue parsing. + if (!buffer.hasRemaining()) + return -1; + + int b = buffer.get() & 0xFF; + _total = Math.addExact(_total, (b & 127) * _multiplier); + _multiplier = Math.multiplyExact(_multiplier, 128); + if ((b & 128) == 0) + { + long total = _total; + reset(); + return total; + } + } + } + + /** + * Reset the internal state of the parser. + */ + public void reset() + { + _prefix = 0; + _total = 0; + _multiplier = 1; + _started = false; + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java new file mode 100644 index 000000000000..f78440f5f2ae --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java @@ -0,0 +1,96 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +/** + * Used to encode integers as described in RFC7541. + */ +public class NBitIntegerEncoder +{ + private NBitIntegerEncoder() + { + } + + /** + * @param prefix the prefix used to encode this long. + * @param value the integer to encode. + * @return the number of octets it would take to encode the long. + */ + public static int octetsNeeded(int prefix, long value) + { + if (prefix <= 0 || prefix > 8) + throw new IllegalArgumentException(); + + int nbits = 0xFF >>> (8 - prefix); + value = value - nbits; + if (value < 0) + return 1; + if (value == 0) + return 2; + int lz = Long.numberOfLeadingZeros(value); + int log = 64 - lz; + + // The return value is 1 for the prefix + the number of 7-bit groups necessary to encode the value. + return 1 + (log + 6) / 7; + } + + /** + * + * @param buffer the buffer to encode into. + * @param prefix the prefix used to encode this long. + * @param value the long to encode into the buffer. + */ + public static void encode(ByteBuffer buffer, int prefix, long value) + { + if (prefix <= 0 || prefix > 8) + throw new IllegalArgumentException(); + + // If prefix is 8 we add an empty byte as we initially modify last byte from the buffer. + if (prefix == 8) + buffer.put((byte)0x00); + + int bits = 0xFF >>> (8 - prefix); + int p = buffer.position() - 1; + if (value < bits) + { + buffer.put(p, (byte)((buffer.get(p) & ~bits) | value)); + } + else + { + buffer.put(p, (byte)(buffer.get(p) | bits)); + long length = value - bits; + while (true) + { + // The value of ~0x7F is different to 0x80 because of all the 1s from the MSB. + if ((length & ~0x7FL) == 0) + { + buffer.put((byte)length); + return; + } + else + { + buffer.put((byte)((length & 0x7F) | 0x80)); + length >>>= 7; + } + } + } + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java new file mode 100644 index 000000000000..a8715976739a --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java @@ -0,0 +1,138 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.CharsetStringBuilder; + +/** + *

Used to decode string literals as described in RFC7541.

+ * + *

The string literal representation consists of a single bit to indicate whether huffman encoding is used, + * followed by the string byte length encoded with the n-bit integer representation also from RFC7541, and + * the bytes of the string are directly after this.

+ * + *

Characters which are illegal field-vchar values are replaced with + * either ' ' or '?' as described in RFC9110

+ */ +public class NBitStringDecoder +{ + private final NBitIntegerDecoder _integerDecoder; + private final HuffmanDecoder _huffmanBuilder; + private final CharsetStringBuilder.Iso88591StringBuilder _builder; + private boolean _huffman; + private int _count; + private int _length; + private int _prefix; + + private State _state = State.PARSING; + + private enum State + { + PARSING, + LENGTH, + VALUE + } + + public NBitStringDecoder() + { + _integerDecoder = new NBitIntegerDecoder(); + _huffmanBuilder = new HuffmanDecoder(); + _builder = new CharsetStringBuilder.Iso88591StringBuilder(); + } + + /** + * Set the prefix length in of the string representation in bits. + * A prefix of 6 means the string representation starts after the first 2 bits. + * @param prefix the number of bits in the string prefix. + */ + public void setPrefix(int prefix) + { + if (_state != State.PARSING) + throw new IllegalStateException(); + _prefix = prefix; + } + + /** + * Decode a string from the buffer. If the buffer does not contain the complete string representation + * then a value of null is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded string. + * @return the decoded string or null to indicate that more data is needed. + * @throws ArithmeticException if the string length value overflows a int. + * @throws EncodingException if the string encoding is invalid. + */ + public String decode(ByteBuffer buffer) throws EncodingException + { + while (true) + { + switch (_state) + { + case PARSING: + byte firstByte = buffer.get(buffer.position()); + _huffman = ((0x80 >>> (8 - _prefix)) & firstByte) != 0; + _state = State.LENGTH; + _integerDecoder.setPrefix(_prefix - 1); + continue; + + case LENGTH: + _length = _integerDecoder.decodeInt(buffer); + if (_length < 0) + return null; + _state = State.VALUE; + _huffmanBuilder.setLength(_length); + continue; + + case VALUE: + String value = _huffman ? _huffmanBuilder.decode(buffer) : stringDecode(buffer); + if (value != null) + reset(); + return value; + + default: + throw new IllegalStateException(_state.name()); + } + } + } + + private String stringDecode(ByteBuffer buffer) + { + for (; _count < _length; _count++) + { + if (!buffer.hasRemaining()) + return null; + _builder.append(buffer.get()); + } + + return _builder.build(); + } + + public void reset() + { + _state = State.PARSING; + _integerDecoder.reset(); + _huffmanBuilder.reset(); + _builder.reset(); + _prefix = 0; + _count = 0; + _length = 0; + _huffman = false; + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java new file mode 100644 index 000000000000..5729ec068314 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java @@ -0,0 +1,82 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http.HttpTokens; + +public class NBitStringEncoder +{ + private NBitStringEncoder() + { + } + + public static int octetsNeeded(int prefix, String value, boolean huffman) + { + if (prefix <= 0 || prefix > 8) + throw new IllegalArgumentException(); + + int contentPrefix = (prefix == 1) ? 8 : prefix - 1; + int encodedValueSize = huffman ? HuffmanEncoder.octetsNeeded(value) : value.length(); + int encodedLengthSize = NBitIntegerEncoder.octetsNeeded(contentPrefix, encodedValueSize); + + // If prefix was 1, then we count an extra byte needed for the prefix. + return encodedLengthSize + encodedValueSize + (prefix == 1 ? 1 : 0); + } + + public static void encode(ByteBuffer buffer, int prefix, String value, boolean huffman) + { + if (prefix <= 0 || prefix > 8) + throw new IllegalArgumentException(); + + byte huffmanFlag = huffman ? (byte)(0x01 << (prefix - 1)) : (byte)0x00; + if (prefix == 8) + { + buffer.put(huffmanFlag); + } + else + { + int p = buffer.position() - 1; + buffer.put(p, (byte)(buffer.get(p) | huffmanFlag)); + } + + // Start encoding size & content in rest of prefix. + // If prefix was 1 we set it back to 8 to indicate to start on a new byte. + prefix = (prefix == 1) ? 8 : prefix - 1; + + if (huffman) + { + int encodedValueSize = HuffmanEncoder.octetsNeeded(value); + NBitIntegerEncoder.encode(buffer, prefix, encodedValueSize); + HuffmanEncoder.encode(buffer, value); + } + else + { + int encodedValueSize = value.length(); + NBitIntegerEncoder.encode(buffer, prefix, encodedValueSize); + for (int i = 0; i < encodedValueSize; i++) + { + char c = value.charAt(i); + c = HttpTokens.sanitizeFieldVchar(c); + buffer.put((byte)c); + } + } + } +} diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java new file mode 100644 index 000000000000..e9e5ae86ecfe --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java @@ -0,0 +1,168 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HuffmanTest +{ + public static String decode(ByteBuffer buffer, int length) throws EncodingException + { + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(length); + String decoded = huffmanDecoder.decode(buffer); + if (decoded == null) + throw new EncodingException("invalid string encoding"); + + huffmanDecoder.reset(); + return decoded; + } + + public static Stream data() + { + return Stream.of( + new String[][]{ + {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, + {"D.4.2", "a8eb10649cbf", "no-cache"}, + {"D.6.1k", "6402", "302"}, + {"D.6.1v", "aec3771a4b", "private"}, + {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, + {"D.6.2te", "640cff", "303"}, + }).map(Arguments::of); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + public void testDecode(String specSection, String hex, String expected) throws Exception + { + byte[] encoded = TypeUtil.fromHexString(hex); + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(encoded.length); + String decoded = huffmanDecoder.decode(ByteBuffer.wrap(encoded)); + assertEquals(expected, decoded, specSection); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + public void testEncode(String specSection, String hex, String expected) + { + ByteBuffer buf = BufferUtil.allocate(1024); + int pos = BufferUtil.flipToFill(buf); + HuffmanEncoder.encode(buf, expected); + BufferUtil.flipToFlush(buf, pos); + String encoded = TypeUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH); + assertEquals(hex, encoded, specSection); + assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected)); + } + + public static Stream testDecode8859OnlyArguments() + { + return Stream.of( + // These are valid characters for ISO-8859-1. + Arguments.of("FfFe6f", (char)128), + Arguments.of("FfFfFbBf", (char)255), + + // RFC9110 specifies these to be replaced as ' ' during decoding. + Arguments.of("FfC7", ' '), // (char)0 + Arguments.of("FfFfFfF7", ' '), // '\r' + Arguments.of("FfFfFfF3", ' '), // '\n' + + // We replace control chars with the default replacement character of '?'. + Arguments.of("FfFfFfBf", '?') // (char)(' ' - 1) + ); + } + + @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name + @MethodSource("testDecode8859OnlyArguments") + public void testDecode8859Only(String hexString, char expected) throws Exception + { + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(hexString)); + String decoded = decode(buffer, buffer.remaining()); + assertThat(decoded, equalTo("" + expected)); + } + + public static Stream testEncode8859OnlyArguments() + { + return Stream.of( + Arguments.of((char)128, (char)128), + Arguments.of((char)255, (char)255), + Arguments.of((char)0, null), + Arguments.of('\r', null), + Arguments.of('\n', null), + Arguments.of((char)456, null), + Arguments.of((char)256, null), + Arguments.of((char)-1, null), + Arguments.of((char)(' ' - 1), null) + ); + } + + @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name + @MethodSource("testEncode8859OnlyArguments") + public void testEncode8859Only(char value, Character expectedValue) throws Exception + { + String s = "value = '" + value + "'"; + + // If expected is null we should not be able to encode. + if (expectedValue == null) + { + assertThat(HuffmanEncoder.octetsNeeded(s), equalTo(-1)); + assertThrows(Throwable.class, () -> encode(s)); + return; + } + + String expected = "value = '" + expectedValue + "'"; + assertThat(HuffmanEncoder.octetsNeeded(s), greaterThan(0)); + ByteBuffer buffer = encode(s); + String decode = decode(buffer); + System.err.println("decoded: " + decode); + assertThat(decode, equalTo(expected)); + } + + private ByteBuffer encode(String s) + { + ByteBuffer buffer = BufferUtil.allocate(32); + BufferUtil.clearToFill(buffer); + HuffmanEncoder.encode(buffer, s); + BufferUtil.flipToFlush(buffer, 0); + return buffer; + } + + private String decode(ByteBuffer buffer) throws Exception + { + return decode(buffer, buffer.remaining()); + } +} diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java similarity index 77% rename from jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java rename to jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java index 84353117c461..03f7b5f2172b 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java @@ -16,33 +16,37 @@ // ======================================================================== // -package org.eclipse.jetty.http2.hpack; +package org.eclipse.jetty.http; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +@SuppressWarnings("PointlessArithmeticExpression") public class NBitIntegerTest { + private final NBitIntegerDecoder _decoder = new NBitIntegerDecoder(); @Test public void testOctetsNeeded() { - assertEquals(0, NBitInteger.octectsNeeded(5, 10)); - assertEquals(2, NBitInteger.octectsNeeded(5, 1337)); - assertEquals(1, NBitInteger.octectsNeeded(8, 42)); - assertEquals(3, NBitInteger.octectsNeeded(8, 1337)); - - assertEquals(0, NBitInteger.octectsNeeded(6, 62)); - assertEquals(1, NBitInteger.octectsNeeded(6, 63)); - assertEquals(1, NBitInteger.octectsNeeded(6, 64)); - assertEquals(2, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); - assertEquals(3, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); - assertEquals(4, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); + assertEquals(1, NBitIntegerEncoder.octetsNeeded(5, 10)); + assertEquals(3, NBitIntegerEncoder.octetsNeeded(5, 1337)); + assertEquals(1, NBitIntegerEncoder.octetsNeeded(8, 42)); + assertEquals(3, NBitIntegerEncoder.octetsNeeded(8, 1337)); + + assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 62)); + assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 63)); + assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 64)); + assertEquals(3, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); + assertEquals(4, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); + assertEquals(5, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); } @Test @@ -83,12 +87,12 @@ public void testEncode(int n, int i, String expected) int p = BufferUtil.flipToFill(buf); if (n < 8) buf.put((byte)0x00); - NBitInteger.encode(buf, n, i); + NBitIntegerEncoder.encode(buf, n, i); BufferUtil.flipToFlush(buf, p); String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); assertEquals(expected, r); - assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitInteger.octectsNeeded(n, i)); + assertEquals(expected.length() / 2, NBitIntegerEncoder.octetsNeeded(n, i)); } @Test @@ -126,8 +130,8 @@ public void testDecode() public void testDecode(int n, int expected, String encoded) { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); - buf.position(n == 8 ? 0 : 1); - assertEquals(expected, NBitInteger.decode(buf, n)); + _decoder.setPrefix(n); + assertEquals(expected, _decoder.decodeInt(buf)); } @Test @@ -137,7 +141,7 @@ public void testEncodeExampleD11() int p = BufferUtil.flipToFill(buf); buf.put((byte)0x77); buf.put((byte)0xFF); - NBitInteger.encode(buf, 5, 10); + NBitIntegerEncoder.encode(buf, 5, 10); BufferUtil.flipToFlush(buf, p); String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); @@ -149,9 +153,9 @@ public void testEncodeExampleD11() public void testDecodeExampleD11() { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("77EaFF")); - buf.position(2); - - assertEquals(10, NBitInteger.decode(buf, 5)); + buf.position(1); + _decoder.setPrefix(5); + assertEquals(10, _decoder.decodeInt(buf)); } @Test @@ -161,11 +165,10 @@ public void testEncodeExampleD12() int p = BufferUtil.flipToFill(buf); buf.put((byte)0x88); buf.put((byte)0x00); - NBitInteger.encode(buf, 5, 1337); + NBitIntegerEncoder.encode(buf, 5, 1337); BufferUtil.flipToFlush(buf, p); String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); - assertEquals("881f9a0a", r); } @@ -173,9 +176,9 @@ public void testEncodeExampleD12() public void testDecodeExampleD12() { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("881f9a0aff")); - buf.position(2); - - assertEquals(1337, NBitInteger.decode(buf, 5)); + buf.position(1); + _decoder.setPrefix(5); + assertEquals(1337, _decoder.decodeInt(buf)); } @Test @@ -185,7 +188,7 @@ public void testEncodeExampleD13() int p = BufferUtil.flipToFill(buf); buf.put((byte)0x88); buf.put((byte)0xFF); - NBitInteger.encode(buf, 8, 42); + NBitIntegerEncoder.encode(buf, 8, 42); BufferUtil.flipToFlush(buf, p); String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); @@ -198,7 +201,7 @@ public void testDecodeExampleD13() { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("882aFf")); buf.position(1); - - assertEquals(42, NBitInteger.decode(buf, 8)); + _decoder.setPrefix(8); + assertEquals(42, _decoder.decodeInt(buf)); } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java index ea6b58458a8f..c3bd42bc2891 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java @@ -29,6 +29,8 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.Trie; @@ -153,7 +155,7 @@ public class HpackContext case C_STATUS: { - entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, Integer.valueOf(value))); + entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, value)); break; } @@ -461,21 +463,21 @@ public static class StaticEntry extends Entry super(field); _slot = index; String value = field.getValue(); - if (value != null && value.length() > 0) + if (value != null && !value.isEmpty()) { - int huffmanLen = Huffman.octetsNeeded(value); + int huffmanLen = HuffmanEncoder.octetsNeeded(value); if (huffmanLen < 0) throw new IllegalStateException("bad value"); - int lenLen = NBitInteger.octectsNeeded(7, huffmanLen); - _huffmanValue = new byte[1 + lenLen + huffmanLen]; + int lenLen = NBitIntegerEncoder.octetsNeeded(7, huffmanLen); + _huffmanValue = new byte[lenLen + huffmanLen]; ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); // Indicate Huffman buffer.put((byte)0x80); // Add huffman length - NBitInteger.encode(buffer, 7, huffmanLen); + NBitIntegerEncoder.encode(buffer, 7, huffmanLen); // Encode value - Huffman.encode(buffer, value); + HuffmanEncoder.encode(buffer, value); } else _huffmanValue = null; diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java index 493b4f425d72..d888d5a40194 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java @@ -24,8 +24,12 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTokens; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; -import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.CharsetStringBuilder; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -41,6 +45,8 @@ public class HpackDecoder private final HpackContext _context; private final MetaDataBuilder _builder; + private final HuffmanDecoder _huffmanDecoder; + private final NBitIntegerDecoder _integerDecoder; private int _localMaxDynamicTableSize; /** @@ -52,6 +58,8 @@ public HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize) _context = new HpackContext(localMaxDynamicTableSize); _localMaxDynamicTableSize = localMaxDynamicTableSize; _builder = new MetaDataBuilder(maxHeaderSize); + _huffmanDecoder = new HuffmanDecoder(); + _integerDecoder = new NBitIntegerDecoder(); } public HpackContext getHpackContext() @@ -69,27 +77,22 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException if (LOG.isDebugEnabled()) LOG.debug(String.format("CtxTbl[%x] decoding %d octets", _context.hashCode(), buffer.remaining())); - // If the buffer is big, don't even think about decoding it - if (buffer.remaining() > _builder.getMaxSize()) - throw new HpackException.SessionException("431 Request Header Fields too large"); + // If the buffer is larger than the max headers size, don't even start decoding it. + int maxSize = _builder.getMaxSize(); + if (maxSize > 0 && buffer.remaining() > maxSize) + throw new HpackException.SessionException("Header fields size too large"); boolean emitted = false; - while (buffer.hasRemaining()) { - if (LOG.isDebugEnabled() && buffer.hasArray()) - { - int l = Math.min(buffer.remaining(), 32); - LOG.debug("decode {}{}", - TypeUtil.toHexString(buffer.array(), buffer.arrayOffset() + buffer.position(), l), - l < buffer.remaining() ? "..." : ""); - } + if (LOG.isDebugEnabled()) + LOG.debug("decode {}", BufferUtil.toHexString(buffer)); byte b = buffer.get(); if (b < 0) { // 7.1 indexed if the high bit is set - int index = NBitInteger.decode(buffer, 7); + int index = integerDecode(buffer, 7); Entry entry = _context.get(index); if (entry == null) throw new HpackException.SessionException("Unknown index %d", index); @@ -130,11 +133,11 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException case 2: // 7.3 case 3: // 7.3 // change table size - int size = NBitInteger.decode(buffer, 5); + int size = integerDecode(buffer, 5); if (LOG.isDebugEnabled()) - LOG.debug("decode resize=" + size); + LOG.debug("decode resize={}", size); if (size > _localMaxDynamicTableSize) - throw new IllegalArgumentException(); + throw new HpackException.CompressionException("Dynamic table resize exceeded max limit"); if (emitted) throw new HpackException.CompressionException("Dynamic table resize after fields"); _context.resize(size); @@ -143,7 +146,7 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException case 0: // 7.2.2 case 1: // 7.2.3 indexed = false; - nameIndex = NBitInteger.decode(buffer, 4); + nameIndex = integerDecode(buffer, 4); break; case 4: // 7.2.1 @@ -151,7 +154,7 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException case 6: // 7.2.1 case 7: // 7.2.1 indexed = true; - nameIndex = NBitInteger.decode(buffer, 6); + nameIndex = integerDecode(buffer, 6); break; default: @@ -170,12 +173,11 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException else { huffmanName = (buffer.get() & 0x80) == 0x80; - int length = NBitInteger.decode(buffer, 7); - _builder.checkSize(length, huffmanName); + int length = integerDecode(buffer, 7); if (huffmanName) - name = Huffman.decode(buffer, length); + name = huffmanDecode(buffer, length); else - name = toASCIIString(buffer, length); + name = toISO88591String(buffer, length); check: for (int i = name.length(); i-- > 0; ) { @@ -211,12 +213,11 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException // decode the value boolean huffmanValue = (buffer.get() & 0x80) == 0x80; - int length = NBitInteger.decode(buffer, 7); - _builder.checkSize(length, huffmanValue); + int length = integerDecode(buffer, 7); if (huffmanValue) - value = Huffman.decode(buffer, length); + value = huffmanDecode(buffer, length); else - value = toASCIIString(buffer, length); + value = toISO88591String(buffer, length); // Make the new field HttpField field; @@ -277,19 +278,61 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException return _builder.build(); } - public static String toASCIIString(ByteBuffer buffer, int length) + private int integerDecode(ByteBuffer buffer, int prefix) throws HpackException.CompressionException + { + try + { + if (prefix != 8) + buffer.position(buffer.position() - 1); + + _integerDecoder.setPrefix(prefix); + int decodedInt = _integerDecoder.decodeInt(buffer); + if (decodedInt < 0) + throw new EncodingException("invalid integer encoding"); + return decodedInt; + } + catch (EncodingException e) + { + HpackException.CompressionException compressionException = new HpackException.CompressionException(e.getMessage()); + compressionException.initCause(e); + throw compressionException; + } + finally + { + _integerDecoder.reset(); + } + } + + private String huffmanDecode(ByteBuffer buffer, int length) throws HpackException.CompressionException + { + try + { + _huffmanDecoder.setLength(length); + String decoded = _huffmanDecoder.decode(buffer); + if (decoded == null) + throw new HpackException.CompressionException("invalid string encoding"); + return decoded; + } + catch (EncodingException e) + { + HpackException.CompressionException compressionException = new HpackException.CompressionException(e.getMessage()); + compressionException.initCause(e); + throw compressionException; + } + finally + { + _huffmanDecoder.reset(); + } + } + + public static String toISO88591String(ByteBuffer buffer, int length) { - StringBuilder builder = new StringBuilder(length); - int position = buffer.position(); - int start = buffer.arrayOffset() + position; - int end = start + length; - buffer.position(position + length); - byte[] array = buffer.array(); - for (int i = start; i < end; i++) + CharsetStringBuilder.Iso88591StringBuilder builder = new CharsetStringBuilder.Iso88591StringBuilder(); + for (int i = 0; i < length; ++i) { - builder.append((char)(0x7f & array[i])); + builder.append(HttpTokens.sanitizeFieldVchar((char)buffer.get())); } - return builder.toString(); + return builder.build(); } @Override diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index 101125207e25..18774e7b8fe7 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; @@ -34,10 +33,13 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; +import org.eclipse.jetty.http.compression.NBitStringEncoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -255,13 +257,9 @@ else if (metadata.isResponse()) } } - // Check size - if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize) - { - LOG.warn("Header list size too large {} > {} for {}", _headerListSize, _maxHeaderListSize); - if (LOG.isDebugEnabled()) - LOG.debug("metadata={}", metadata); - } + int maxHeaderListSize = getMaxHeaderListSize(); + if (maxHeaderListSize > 0 && _headerListSize > maxHeaderListSize) + throw new HpackException.SessionException("Header size %d > %d", _headerListSize, maxHeaderListSize); if (LOG.isDebugEnabled()) LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos)); @@ -278,13 +276,11 @@ else if (metadata.isResponse()) } } - public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize) + public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxTableSize) { - if (maxDynamicTableSize > _remoteMaxDynamicTableSize) - throw new IllegalArgumentException(); buffer.put((byte)0x20); - NBitInteger.encode(buffer, 5, maxDynamicTableSize); - _context.resize(maxDynamicTableSize); + NBitIntegerEncoder.encode(buffer, 5, maxTableSize); + _context.resize(maxTableSize); } public void encode(ByteBuffer buffer, HttpField field) @@ -295,8 +291,6 @@ public void encode(ByteBuffer buffer, HttpField field) int fieldSize = field.getName().length() + field.getValue().length(); _headerListSize += fieldSize + 32; - final int p = _debug ? buffer.position() : -1; - String encoding = null; // Is there an index entry for the field? @@ -314,9 +308,9 @@ public void encode(ByteBuffer buffer, HttpField field) { int index = _context.index(entry); buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, index); + NBitIntegerEncoder.encode(buffer, 7, index); if (_debug) - encoding = "IdxField" + (entry.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(7, index)); + encoding = "IdxField" + (entry.isStatic() ? "S" : "") + NBitIntegerEncoder.octetsNeeded(7, index); } } else @@ -390,19 +384,19 @@ else if (DO_NOT_INDEX.contains(header)) if (_debug) encoding = "Lit" + - ((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(4, _context.index(name))))) + + ((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(4, _context.index(name))))) + (huffman ? "HuffV" : "LitV") + (neverIndex ? "!!Idx" : "!Idx"); } else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && !"0".equals(field.getValue())) { - // The field is too large or a non zero content length, so do not index. + // The field is too large or a non-zero content length, so do not index. indexed = false; encodeName(buffer, (byte)0x00, 4, header.asString(), name); encodeValue(buffer, true, field.getValue()); if (_debug) encoding = "Lit" + - ((name == null) ? "HuffN" : "IdxNS" + (1 + NBitInteger.octectsNeeded(4, _context.index(name)))) + + ((name == null) ? "HuffN" : "IdxNS" + (1 + NBitIntegerEncoder.octetsNeeded(4, _context.index(name)))) + "HuffV!Idx"; } else @@ -413,7 +407,7 @@ else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader. encodeName(buffer, (byte)0x40, 6, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); if (_debug) - encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(6, _context.index(name))))) + + encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(6, _context.index(name))))) + (huffman ? "HuffVIdx" : "LitVIdx"); } } @@ -425,10 +419,8 @@ else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader. if (_debug) { - byte[] bytes = new byte[buffer.position() - p]; - buffer.position(p); - buffer.get(bytes); - LOG.debug("encode {}:'{}' to '{}'", encoding, field, TypeUtil.toHexString(bytes)); + if (LOG.isDebugEnabled()) + LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString((ByteBuffer)buffer.duplicate().flip())); } } @@ -440,55 +432,17 @@ private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Ent // leave name index bits as 0 // Encode the name always with lowercase huffman buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); - Huffman.encodeLC(buffer, name); + NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowerCase(name)); + HuffmanEncoder.encodeLowerCase(buffer, name); } else { - NBitInteger.encode(buffer, bits, _context.index(entry)); + NBitIntegerEncoder.encode(buffer, bits, _context.index(entry)); } } static void encodeValue(ByteBuffer buffer, boolean huffman, String value) { - if (huffman) - { - // huffman literal value - buffer.put((byte)0x80); - - int needed = Huffman.octetsNeeded(value); - if (needed >= 0) - { - NBitInteger.encode(buffer, 7, needed); - Huffman.encode(buffer, value); - } - else - { - // Not iso_8859_1 - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes)); - Huffman.encode(buffer, bytes); - } - } - else - { - // add literal assuming iso_8859_1 - buffer.put((byte)0x00).mark(); - NBitInteger.encode(buffer, 7, value.length()); - for (int i = 0; i < value.length(); i++) - { - char c = value.charAt(i); - if (c < ' ' || c > 127) - { - // Not iso_8859_1, so re-encode as UTF-8 - buffer.reset(); - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - NBitInteger.encode(buffer, 7, bytes.length); - buffer.put(bytes, 0, bytes.length); - return; - } - buffer.put((byte)c); - } - } + NBitStringEncoder.encode(buffer, 8, value, huffman); } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java index 2aad1221d2f5..df89d802c693 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.http2.hpack; -@SuppressWarnings("serial") public abstract class HpackException extends Exception { HpackException(String messageFormat, Object... args) @@ -35,7 +34,7 @@ public abstract class HpackException extends Exception */ public static class StreamException extends HpackException { - StreamException(String messageFormat, Object... args) + public StreamException(String messageFormat, Object... args) { super(messageFormat, args); } @@ -48,7 +47,7 @@ public static class StreamException extends HpackException */ public static class SessionException extends HpackException { - SessionException(String messageFormat, Object... args) + public SessionException(String messageFormat, Object... args) { super(messageFormat, args); } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java index 769819dff3fb..629ef2c5fe82 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java @@ -23,6 +23,8 @@ import org.eclipse.jetty.http.HttpFieldPreEncoder; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.BufferUtil; /** @@ -31,18 +33,12 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder { - /** - * @see org.eclipse.jetty.http.HttpFieldPreEncoder#getHttpVersion() - */ @Override public HttpVersion getHttpVersion() { return HttpVersion.HTTP_2; } - /** - * @see org.eclipse.jetty.http.HttpFieldPreEncoder#getEncodedField(org.eclipse.jetty.http.HttpHeader, java.lang.String, java.lang.String) - */ @Override public byte[] getEncodedField(HttpHeader header, String name, String value) { @@ -78,12 +74,12 @@ else if (header == HttpHeader.CONTENT_LENGTH && value.length() > 1) int nameIdx = HpackContext.staticIndex(header); if (nameIdx > 0) - NBitInteger.encode(buffer, bits, nameIdx); + NBitIntegerEncoder.encode(buffer, bits, nameIdx); else { buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); - Huffman.encodeLC(buffer, name); + NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowerCase(name)); + HuffmanEncoder.encodeLowerCase(buffer, name); } HpackEncoder.encodeValue(buffer, huffman, value); diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index df45309f011c..9ed7253f52d2 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -29,7 +29,7 @@ public class MetaDataBuilder { - private final int _maxSize; + private int _maxSize; private int _size; private Integer _status; private String _method; @@ -60,6 +60,11 @@ public int getMaxSize() return _maxSize; } + public void setMaxSize(int maxSize) + { + _maxSize = maxSize; + } + /** * Get the size. * @@ -70,17 +75,18 @@ public int getSize() return _size; } - public void emit(HttpField field) throws HpackException.SessionException + public void emit(HttpField field) throws SessionException { HttpHeader header = field.getHeader(); String name = field.getName(); - if (name == null || name.length() == 0) - throw new HpackException.SessionException("Header size 0"); + if (name == null || name.isEmpty()) + throw new SessionException("Header size 0"); String value = field.getValue(); int fieldSize = name.length() + (value == null ? 0 : value.length()); _size += fieldSize + 32; - if (_size > _maxSize) - throw new HpackException.SessionException("Header size %d > %d", _size, _maxSize); + int maxSize = getMaxSize(); + if (maxSize > 0 && _size > maxSize) + throw new SessionException("Header size %d > %d", _size, maxSize); if (field instanceof StaticTableHttpField) { @@ -89,7 +95,7 @@ public void emit(HttpField field) throws HpackException.SessionException { case C_STATUS: if (checkPseudoHeader(header, _status)) - _status = (Integer)staticField.getStaticValue(); + _status = staticField.getIntValue(); _response = true; break; @@ -157,7 +163,7 @@ else if (value != null) case C_PATH: if (checkPseudoHeader(header, _path)) { - if (value != null && value.length() > 0) + if (value != null && !value.isEmpty()) _path = value; else streamException("No Path"); @@ -201,7 +207,7 @@ else if (value != null) } } - protected void streamException(String messageFormat, Object... args) + public void streamException(String messageFormat, Object... args) { HpackException.StreamException stream = new HpackException.StreamException(messageFormat, args); if (_streamException == null) @@ -267,23 +273,7 @@ public MetaData build() throws HpackException.StreamException _authority = null; _path = null; _size = 0; - _contentLength = Long.MIN_VALUE; + _contentLength = -1; } } - - /** - * Check that the max size will not be exceeded. - * - * @param length the length - * @param huffman the huffman name - * @throws SessionException in case of size errors - */ - public void checkSize(int length, boolean huffman) throws SessionException - { - // Apply a huffman fudge factor - if (huffman) - length = (length * 4) / 3; - if ((_size + length) > _maxSize) - throw new HpackException.SessionException("Header too large %d > %d", _size + length, _maxSize); - } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java deleted file mode 100644 index bab61bbd4ae9..000000000000 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java +++ /dev/null @@ -1,151 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.http2.hpack; - -import java.nio.ByteBuffer; - -public class NBitInteger -{ - public static int octectsNeeded(int n, int i) - { - if (n == 8) - { - int nbits = 0xFF; - i = i - nbits; - if (i < 0) - return 1; - if (i == 0) - return 2; - int lz = Integer.numberOfLeadingZeros(i); - int log = 32 - lz; - return 1 + (log + 6) / 7; - } - - int nbits = 0xFF >>> (8 - n); - i = i - nbits; - if (i < 0) - return 0; - if (i == 0) - return 1; - int lz = Integer.numberOfLeadingZeros(i); - int log = 32 - lz; - return (log + 6) / 7; - } - - public static void encode(ByteBuffer buf, int n, int i) - { - if (n == 8) - { - if (i < 0xFF) - { - buf.put((byte)i); - } - else - { - buf.put((byte)0xFF); - - int length = i - 0xFF; - while (true) - { - if ((length & ~0x7F) == 0) - { - buf.put((byte)length); - return; - } - else - { - buf.put((byte)((length & 0x7F) | 0x80)); - length >>>= 7; - } - } - } - } - else - { - int p = buf.position() - 1; - int bits = 0xFF >>> (8 - n); - - if (i < bits) - { - buf.put(p, (byte)((buf.get(p) & ~bits) | i)); - } - else - { - buf.put(p, (byte)(buf.get(p) | bits)); - - int length = i - bits; - while (true) - { - if ((length & ~0x7F) == 0) - { - buf.put((byte)length); - return; - } - else - { - buf.put((byte)((length & 0x7F) | 0x80)); - length >>>= 7; - } - } - } - } - } - - public static int decode(ByteBuffer buffer, int n) - { - if (n == 8) - { - int nbits = 0xFF; - - int i = buffer.get() & 0xff; - - if (i == nbits) - { - int m = 1; - int b; - do - { - b = 0xff & buffer.get(); - i = i + (b & 127) * m; - m = m * 128; - } - while ((b & 128) == 128); - } - return i; - } - - int nbits = 0xFF >>> (8 - n); - - int i = buffer.get(buffer.position() - 1) & nbits; - - if (i == nbits) - { - int m = 1; - int b; - do - { - b = 0xff & buffer.get(); - i = i + (b & 127) * m; - m = m * 128; - } - while ((b & 128) == 128); - } - return i; - } -} diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java index ed0d914c8e2a..a767a51fd2fd 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java @@ -21,6 +21,9 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -37,6 +40,32 @@ */ public class HpackContextTest { + public static String decode(ByteBuffer buffer, int length) throws EncodingException + { + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(length); + String decoded = huffmanDecoder.decode(buffer); + if (decoded == null) + throw new EncodingException("invalid string encoding"); + + huffmanDecoder.reset(); + return decoded; + } + + public static int decodeInt(ByteBuffer buffer, int prefix) throws EncodingException + { + // This is a fix for HPACK as it already takes the first byte of the encoded integer. + if (prefix != 8) + buffer.position(buffer.position() - 1); + + NBitIntegerDecoder decoder = new NBitIntegerDecoder(); + decoder.setPrefix(prefix); + int decodedInt = decoder.decodeInt(buffer); + if (decodedInt < 0) + throw new EncodingException("invalid integer encoding"); + decoder.reset(); + return decodedInt; + } @Test public void testStaticName() @@ -428,10 +457,10 @@ public void testStaticHuffmanValues() throws Exception int huff = 0xff & buffer.get(); assertTrue((0x80 & huff) == 0x80); - int len = NBitInteger.decode(buffer, 7); + int len = decodeInt(buffer, 7); assertEquals(len, buffer.remaining()); - String value = Huffman.decode(buffer); + String value = decode(buffer, buffer.remaining()); assertEquals(entry.getHttpField().getValue(), value); } diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java index 4cc50c593177..b75c713995f7 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java @@ -470,7 +470,7 @@ public void testHuffmanEncodedExtraPadding() String encoded = "82868441" + "84" + "49509FFF"; ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } /* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */ @@ -483,7 +483,7 @@ public void testHuffmanEncodedZeroPadding() ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Incorrect padding")); + assertThat(ex.getMessage(), Matchers.containsString("incorrect_padding")); } /* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */ @@ -496,7 +496,7 @@ public void testHuffmanEncodedWithEOS() ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("EOS in content")); + assertThat(ex.getMessage(), Matchers.containsString("eos_in_content")); } @Test @@ -508,7 +508,7 @@ public void testHuffmanEncodedOneIncompleteOctet() ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } @Test @@ -520,7 +520,7 @@ public void testHuffmanEncodedTwoIncompleteOctet() ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } @Test diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java index 72155e51aff3..5a20d348bebf 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java @@ -133,7 +133,7 @@ public void encodeDecodeTooLargeTest() throws Exception } catch (HpackException.SessionException e) { - assertThat(e.getMessage(), containsString("Header too large")); + assertThat(e.getMessage(), containsString("Header size 198 > 164")); } } @@ -141,21 +141,22 @@ public void encodeDecodeTooLargeTest() throws Exception public void encodeDecodeNonAscii() throws Exception { HpackEncoder encoder = new HpackEncoder(); - HpackDecoder decoder = new HpackDecoder(4096, 8192); ByteBuffer buffer = BufferUtil.allocate(16 * 1024); HttpFields fields0 = new HttpFields(); - // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck + // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck fields0.add("Cookie", "[\uD842\uDF9F]"); fields0.add("custom-key", "[\uD842\uDF9F]"); Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0); - BufferUtil.clearToFill(buffer); - encoder.encode(buffer, original0); - BufferUtil.flipToFlush(buffer, 0); - Response decoded0 = (Response)decoder.decode(buffer); + HpackException.SessionException throwable = assertThrows(HpackException.SessionException.class, () -> + { + BufferUtil.clearToFill(buffer); + encoder.encode(buffer, original0); + BufferUtil.flipToFlush(buffer, 0); + }); - assertMetaDataSame(original0, decoded0); + assertThat(throwable.getMessage(), containsString("Could not hpack encode")); } @Test diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java deleted file mode 100644 index 1b62829e4eb4..000000000000 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java +++ /dev/null @@ -1,87 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.http2.hpack; - -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.util.Locale; -import java.util.stream.Stream; - -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.TypeUtil; -import org.hamcrest.Matchers; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class HuffmanTest -{ - public static Stream data() - { - return Stream.of( - new String[][]{ - {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, - {"D.4.2", "a8eb10649cbf", "no-cache"}, - {"D.6.1k", "6402", "302"}, - {"D.6.1v", "aec3771a4b", "private"}, - {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, - {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, - {"D.6.2te", "640cff", "303"}, - }).map(Arguments::of); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testDecode(String specSection, String hex, String expected) throws Exception - { - byte[] encoded = TypeUtil.fromHexString(hex); - String decoded = Huffman.decode(ByteBuffer.wrap(encoded)); - assertEquals(expected, decoded, specSection); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testEncode(String specSection, String hex, String expected) - { - ByteBuffer buf = BufferUtil.allocate(1024); - int pos = BufferUtil.flipToFill(buf); - Huffman.encode(buf, expected); - BufferUtil.flipToFlush(buf, pos); - String encoded = TypeUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH); - assertEquals(hex, encoded, specSection); - assertEquals(hex.length() / 2, Huffman.octetsNeeded(expected)); - } - - @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name - @ValueSource(chars = {(char)128, (char)0, (char)-1, ' ' - 1}) - public void testEncode8859Only(char bad) - { - String s = "bad '" + bad + "'"; - - assertThat(Huffman.octetsNeeded(s), Matchers.is(-1)); - - assertThrows(BufferOverflowException.class, - () -> Huffman.encode(BufferUtil.allocate(32), s)); - } -} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java new file mode 100644 index 000000000000..13f588e8aa22 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java @@ -0,0 +1,312 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + *

Build a string from a sequence of bytes and/or characters.

+ *

Implementations of this interface are optimized for processing a mix of calls to already decoded + * character based appends (e.g. {@link #append(char)} and calls to undecoded byte methods (e.g. {@link #append(byte)}. + * This is particularly useful for decoding % encoded strings that are mostly already decoded but may contain + * escaped byte sequences that are not decoded. The standard {@link CharsetDecoder} API is not well suited for this + * use-case.

+ *

Any coding errors in the string will be reported by a {@link CharacterCodingException} thrown + * from the {@link #build()} method.

+ * @see Utf8StringBuilder for UTF-8 decoding with replacement of coding errors and/or fast fail behaviour. + * @see CharsetDecoder for decoding arbitrary {@link Charset}s with control over {@link CodingErrorAction}. + */ +public interface CharsetStringBuilder +{ + /** + * @param b An encoded byte to append + */ + void append(byte b); + + /** + * @param c A decoded character to append + */ + void append(char c); + + /** + * @param bytes Array of encoded bytes to append + */ + default void append(byte[] bytes) + { + append(bytes, 0, bytes.length); + } + + /** + * @param b Array of encoded bytes + * @param offset offset into the array + * @param length the number of bytes to append from the array. + */ + default void append(byte[] b, int offset, int length) + { + int end = offset + length; + for (int i = offset; i < end; i++) + append(b[i]); + } + + /** + * @param chars sequence of decoded characters + * @param offset offset into the array + * @param length the number of character to append from the sequence. + */ + default void append(CharSequence chars, int offset, int length) + { + int end = offset + length; + for (int i = offset; i < end; i++) + append(chars.charAt(i)); + } + + /** + * @param buf Buffer of encoded bytes to append. The bytes are consumed from the buffer. + */ + default void append(ByteBuffer buf) + { + int end = buf.position() + buf.remaining(); + while (buf.position() < end) + append(buf.get()); + } + + /** + *

Build the completed string and reset the buffer.

+ * @return The decoded built string which must be complete in regard to any multibyte sequences. + * @throws CharacterCodingException If the bytes cannot be correctly decoded or a multibyte sequence is incomplete. + */ + String build() throws CharacterCodingException; + + void reset(); + + /** + * @param charset The charset + * @return A {@link CharsetStringBuilder} suitable for the charset. + */ + static CharsetStringBuilder forCharset(Charset charset) + { + Objects.requireNonNull(charset); + if (charset == StandardCharsets.ISO_8859_1) + return new Iso88591StringBuilder(); + if (charset == StandardCharsets.US_ASCII) + return new UsAsciiStringBuilder(); + + // Use a CharsetDecoder that defaults to CodingErrorAction#REPORT + return new DecoderStringBuilder(charset.newDecoder()); + } + + class Iso88591StringBuilder implements CharsetStringBuilder + { + private final StringBuilder _builder = new StringBuilder(); + + @Override + public void append(byte b) + { + _builder.append((char)(0xff & b)); + } + + @Override + public void append(char c) + { + _builder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + _builder.append(chars, offset, offset + length); + } + + @Override + public String build() + { + String s = _builder.toString(); + _builder.setLength(0); + return s; + } + + @Override + public void reset() + { + _builder.setLength(0); + } + } + + class UsAsciiStringBuilder implements CharsetStringBuilder + { + private final StringBuilder _builder = new StringBuilder(); + + @Override + public void append(byte b) + { + if (b < 0) + throw new IllegalArgumentException(); + _builder.append((char)b); + } + + @Override + public void append(char c) + { + _builder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + _builder.append(chars, offset, offset + length); + } + + @Override + public String build() + { + String s = _builder.toString(); + _builder.setLength(0); + return s; + } + + @Override + public void reset() + { + _builder.setLength(0); + } + } + + class DecoderStringBuilder implements CharsetStringBuilder + { + private final CharsetDecoder _decoder; + private final StringBuilder _stringBuilder = new StringBuilder(32); + private ByteBuffer _buffer = ByteBuffer.allocate(32); + + public DecoderStringBuilder(CharsetDecoder charsetDecoder) + { + _decoder = charsetDecoder; + } + + private void ensureSpace(int needed) + { + int space = _buffer.remaining(); + if (space < needed) + { + int position = _buffer.position(); + _buffer = ByteBuffer.wrap(Arrays.copyOf(_buffer.array(), _buffer.capacity() + needed - space + 32)); + _buffer.position(position); + } + } + + @Override + public void append(byte b) + { + ensureSpace(1); + _buffer.put(b); + } + + @Override + public void append(char c) + { + if (_buffer.position() > 0) + { + try + { + // Append any data already in the decoder + _buffer.flip(); + _stringBuilder.append(_decoder.decode(_buffer)); + _buffer.clear(); + } + catch (CharacterCodingException e) + { + // This will be thrown only if the decoder is configured to REPORT, + // otherwise errors will be ignored or replaced and we will not catch here. + throw new RuntimeException(e); + } + } + _stringBuilder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + if (_buffer.position() > 0) + { + try + { + // Append any data already in the decoder + _buffer.flip(); + _stringBuilder.append(_decoder.decode(_buffer)); + _buffer.clear(); + } + catch (CharacterCodingException e) + { + // This will be thrown only if the decoder is configured to REPORT, + // otherwise errors will be ignored or replaced and we will not catch here. + throw new RuntimeException(e); + } + } + _stringBuilder.append(chars, offset, offset + length); + } + + @Override + public void append(byte[] b, int offset, int length) + { + ensureSpace(length); + _buffer.put(b, offset, length); + } + + @Override + public void append(ByteBuffer buf) + { + ensureSpace(buf.remaining()); + _buffer.put(buf); + } + + @Override + public String build() throws CharacterCodingException + { + try + { + if (_buffer.position() > 0) + { + _buffer.flip(); + CharSequence decoded = _decoder.decode(_buffer); + _buffer.clear(); + if (_stringBuilder.length() == 0) + return decoded.toString(); + _stringBuilder.append(decoded); + } + return _stringBuilder.toString(); + } + finally + { + _stringBuilder.setLength(0); + } + } + + @Override + public void reset() + { + _stringBuilder.setLength(0); + } + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index 0bdb229eeec4..b0535801e50c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -107,6 +107,27 @@ public static String normalizeCharset(String s, int offset, int length) '\170', '\171', '\172', '\173', '\174', '\175', '\176', '\177' }; + // @checkstyle-disable-check : IllegalTokenTextCheck + private static final char[] uppercases = + { + '\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007', + '\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017', + '\020', '\021', '\022', '\023', '\024', '\025', '\026', '\027', + '\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037', + '\040', '\041', '\042', '\043', '\044', '\045', '\046', '\047', + '\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057', + '\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067', + '\070', '\071', '\072', '\073', '\074', '\075', '\076', '\077', + '\100', '\101', '\102', '\103', '\104', '\105', '\106', '\107', + '\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117', + '\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127', + '\130', '\131', '\132', '\133', '\134', '\135', '\136', '\137', + '\140', '\101', '\102', '\103', '\104', '\105', '\106', '\107', + '\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117', + '\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127', + '\130', '\131', '\132', '\173', '\174', '\175', '\176', '\177' + }; + /** * fast lower case conversion. Only works on ascii (not unicode) * @@ -144,6 +165,42 @@ public static String asciiToLowerCase(String s) return c == null ? s : new String(c); } + /** + * fast upper case conversion. Only works on ascii (not unicode) + * + * @param s the string to convert + * @return a lower case version of s + */ + public static String asciiToUpperCase(String s) + { + if (s == null) + return null; + + char[] c = null; + int i = s.length(); + // look for first conversion + while (i-- > 0) + { + char c1 = s.charAt(i); + if (c1 <= 127) + { + char c2 = uppercases[c1]; + if (c1 != c2) + { + c = s.toCharArray(); + c[i] = c2; + break; + } + } + } + while (i-- > 0) + { + if (c[i] <= 127) + c[i] = uppercases[c[i]]; + } + return c == null ? s : new String(c); + } + /** * Replace all characters from input string that are known to have * special meaning in various filesystems.