blob: 880d0ccbdeeda35015d077e92f07d4c4634fa165 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.core5.http2.hpack;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.ByteArrayBuffer;
/**
* HPACK decoder.
*
* @since 5.0
*/
@Internal
public final class HPackDecoder {
private static final String UNEXPECTED_EOS = "Unexpected end of HPACK data";
private static final String MAX_LIMIT_EXCEEDED = "Max integer exceeded";
private final InboundDynamicTable dynamicTable;
private final ByteArrayBuffer contentBuf;
private final CharsetDecoder charsetDecoder;
private CharBuffer tmpBuf;
private int maxTableSize;
private int maxListSize;
HPackDecoder(final InboundDynamicTable dynamicTable, final CharsetDecoder charsetDecoder) {
this.dynamicTable = dynamicTable != null ? dynamicTable : new InboundDynamicTable();
this.contentBuf = new ByteArrayBuffer(256);
this.charsetDecoder = charsetDecoder;
this.maxTableSize = dynamicTable != null ? dynamicTable.getMaxSize() : Integer.MAX_VALUE;
this.maxListSize = Integer.MAX_VALUE;
}
HPackDecoder(final InboundDynamicTable dynamicTable, final Charset charset) {
this(dynamicTable, charset != null && !StandardCharsets.US_ASCII.equals(charset) ? charset.newDecoder() : null);
}
public HPackDecoder(final Charset charset) {
this(new InboundDynamicTable(), charset);
}
public HPackDecoder(final CharsetDecoder charsetDecoder) {
this(new InboundDynamicTable(), charsetDecoder);
}
static int readByte(final ByteBuffer src) throws HPackException {
if (!src.hasRemaining()) {
throw new HPackException(UNEXPECTED_EOS);
}
return src.get() & 0xff;
}
static int peekByte(final ByteBuffer src) throws HPackException {
if (!src.hasRemaining()) {
throw new HPackException(UNEXPECTED_EOS);
}
final int pos = src.position();
final int b = src.get() & 0xff;
src.position(pos);
return b;
}
static int decodeInt(final ByteBuffer src, final int n) throws HPackException {
final int nbits = 0xff >>> (8 - n);
int value = readByte(src) & nbits;
if (value < nbits) {
return value;
}
int m = 0;
while (m < 32) {
final int b = readByte(src);
if ((b & 0x80) != 0) {
value += (b & 0x7f) << m;
m += 7;
} else {
if (m == 28 && (b & 0xf8) != 0) {
break;
}
value += b << m;
return value;
}
}
throw new HPackException(MAX_LIMIT_EXCEEDED);
}
static void decodePlainString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
final int strLen = decodeInt(src, 7);
final int remaining = src.remaining();
if (strLen > remaining) {
throw new HPackException(UNEXPECTED_EOS);
}
final int originalLimit = src.limit();
src.limit(originalLimit - (remaining - strLen));
buffer.append(src);
src.limit(originalLimit);
}
static void decodeHuffman(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
final int strLen = decodeInt(src, 7);
if (strLen > src.remaining()) {
throw new HPackException(UNEXPECTED_EOS);
}
final int limit = src.limit();
src.limit(src.position() + strLen);
Huffman.DECODER.decode(buffer, src);
src.limit(limit);
}
void decodeString(final ByteArrayBuffer buffer, final ByteBuffer src) throws HPackException {
final int firstByte = peekByte(src);
if ((firstByte & 0x80) == 0x80) {
decodeHuffman(buffer, src);
} else {
decodePlainString(buffer, src);
}
}
int getTmpBufSize() {
return tmpBuf == null ? 0 : tmpBuf.capacity();
}
private void clearState() {
if (this.tmpBuf != null) {
this.tmpBuf.clear();
}
if (this.charsetDecoder != null) {
this.charsetDecoder.reset();
}
this.contentBuf.clear();
}
private void expandCapacity(final int capacity) {
final CharBuffer previous = this.tmpBuf;
this.tmpBuf = CharBuffer.allocate(capacity);
previous.flip();
this.tmpBuf.put(previous);
}
private void ensureCapacity(final int extra) {
if (this.tmpBuf == null) {
this.tmpBuf = CharBuffer.allocate(Math.max(256, extra));
}
final int requiredCapacity = this.tmpBuf.position() + extra;
if (requiredCapacity > this.tmpBuf.capacity()) {
expandCapacity(requiredCapacity);
}
}
int decodeString(final ByteBuffer src, final StringBuilder buf) throws HPackException, CharacterCodingException {
clearState();
decodeString(this.contentBuf, src);
final int binaryLen = this.contentBuf.length();
if (binaryLen == 0) {
return 0;
}
if (this.charsetDecoder == null) {
buf.ensureCapacity(binaryLen);
for (int i = 0; i < binaryLen; i++) {
buf.append((char) (this.contentBuf.byteAt(i) & 0xff));
}
} else {
final ByteBuffer in = ByteBuffer.wrap(this.contentBuf.array(), 0, binaryLen);
while (in.hasRemaining()) {
ensureCapacity(in.remaining());
final CoderResult result = this.charsetDecoder.decode(in, this.tmpBuf, true);
if (result.isError()) {
result.throwException();
}
}
ensureCapacity(8);
final CoderResult result = this.charsetDecoder.flush(this.tmpBuf);
if (result.isError()) {
result.throwException();
}
this.tmpBuf.flip();
buf.append(this.tmpBuf);
}
return binaryLen;
}
HPackHeader decodeLiteralHeader(
final ByteBuffer src,
final HPackRepresentation representation) throws HPackException, CharacterCodingException {
final int n = representation == HPackRepresentation.WITH_INDEXING ? 6 : 4;
final int index = decodeInt(src, n);
final String name;
final int nameLen;
if (index == 0) {
final StringBuilder buf = new StringBuilder();
nameLen = decodeString(src, buf);
name = buf.toString();
} else {
final HPackHeader existing = this.dynamicTable.getHeader(index);
if (existing == null) {
throw new HPackException("Invalid header index");
}
name = existing.getName();
nameLen = existing.getNameLen();
}
final StringBuilder buf = new StringBuilder();
final int valueLen = decodeString(src, buf);
final String value = buf.toString();
final HPackHeader header = new HPackHeader(name, nameLen, value, valueLen, representation == HPackRepresentation.NEVER_INDEXED);
if (representation == HPackRepresentation.WITH_INDEXING) {
this.dynamicTable.add(header);
}
return header;
}
HPackHeader decodeIndexedHeader(final ByteBuffer src) throws HPackException {
final int index = decodeInt(src, 7);
final HPackHeader existing = this.dynamicTable.getHeader(index);
if (existing == null) {
throw new HPackException("Invalid header index");
}
return existing;
}
public Header decodeHeader(final ByteBuffer src) throws HPackException {
final HPackHeader header = decodeHPackHeader(src);
return header != null ? new BasicHeader(header.getName(), header.getValue(), header.isSensitive()) : null;
}
HPackHeader decodeHPackHeader(final ByteBuffer src) throws HPackException {
try {
while (src.hasRemaining()) {
final int b = peekByte(src);
if ((b & 0x80) == 0x80) {
return decodeIndexedHeader(src);
} else if ((b & 0xc0) == 0x40) {
return decodeLiteralHeader(src, HPackRepresentation.WITH_INDEXING);
} else if ((b & 0xf0) == 0x00) {
return decodeLiteralHeader(src, HPackRepresentation.WITHOUT_INDEXING);
} else if ((b & 0xf0) == 0x10) {
return decodeLiteralHeader(src, HPackRepresentation.NEVER_INDEXED);
} else if ((b & 0xe0) == 0x20) {
final int maxSize = decodeInt(src, 5);
this.dynamicTable.setMaxSize(Math.min(this.maxTableSize, maxSize));
} else {
throw new HPackException("Unexpected header first byte: 0x" + Integer.toHexString(b));
}
}
return null;
} catch (final CharacterCodingException ex) {
throw new HPackException(ex.getMessage(), ex);
}
}
public List<Header> decodeHeaders(final ByteBuffer src) throws HPackException {
final boolean enforceSizeLimit = maxListSize < Integer.MAX_VALUE;
int listSize = 0;
final List<Header> list = new ArrayList<>();
while (src.hasRemaining()) {
final HPackHeader header = decodeHPackHeader(src);
if (header == null) {
break;
}
if (enforceSizeLimit) {
listSize += header.getTotalSize();
if (listSize >= maxListSize) {
throw new HeaderListConstraintException("Maximum header list size exceeded");
}
}
list.add(new BasicHeader(header.getName(), header.getValue(), header.isSensitive()));
}
return list;
}
public int getMaxTableSize() {
return this.maxTableSize;
}
public void setMaxTableSize(final int maxTableSize) {
Args.notNegative(maxTableSize, "Max table size");
this.maxTableSize = maxTableSize;
this.dynamicTable.setMaxSize(maxTableSize);
}
public int getMaxListSize() {
return maxListSize;
}
public void setMaxListSize(final int maxListSize) {
Args.notNegative(maxListSize, "Max list size");
this.maxListSize = maxListSize;
}
}