All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.http2.hpack.HpackEncoder Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http2.hpack;

import java.nio.ByteBuffer;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HpackEncoder
{
    private static final Logger LOG = LoggerFactory.getLogger(HpackEncoder.class);
    private static final HttpField[] STATUSES = new HttpField[599];
    static final EnumSet DO_NOT_HUFFMAN =
        EnumSet.of(
            HttpHeader.AUTHORIZATION,
            HttpHeader.CONTENT_MD5,
            HttpHeader.PROXY_AUTHENTICATE,
            HttpHeader.PROXY_AUTHORIZATION);
    static final EnumSet DO_NOT_INDEX =
        EnumSet.of(
            // HttpHeader.C_PATH,  // TODO more data needed
            // HttpHeader.DATE,    // TODO more data needed
            HttpHeader.AUTHORIZATION,
            HttpHeader.CONTENT_MD5,
            HttpHeader.CONTENT_RANGE,
            HttpHeader.ETAG,
            HttpHeader.IF_MODIFIED_SINCE,
            HttpHeader.IF_UNMODIFIED_SINCE,
            HttpHeader.IF_NONE_MATCH,
            HttpHeader.IF_RANGE,
            HttpHeader.IF_MATCH,
            HttpHeader.LOCATION,
            HttpHeader.RANGE,
            HttpHeader.RETRY_AFTER,
            // HttpHeader.EXPIRES,
            HttpHeader.LAST_MODIFIED,
            HttpHeader.SET_COOKIE,
            HttpHeader.SET_COOKIE2);
    static final EnumSet NEVER_INDEX =
        EnumSet.of(
            HttpHeader.AUTHORIZATION,
            HttpHeader.SET_COOKIE,
            HttpHeader.SET_COOKIE2);
    private static final EnumSet IGNORED_HEADERS = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.KEEP_ALIVE,
        HttpHeader.PROXY_CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.UPGRADE);
    private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers");
    private static final PreEncodedHttpField C_SCHEME_HTTP = new PreEncodedHttpField(HttpHeader.C_SCHEME, "http");
    private static final PreEncodedHttpField C_SCHEME_HTTPS = new PreEncodedHttpField(HttpHeader.C_SCHEME, "https");
    private static final EnumMap C_METHODS = new EnumMap<>(HttpMethod.class);

    static
    {
        for (HttpStatus.Code code : HttpStatus.Code.values())
        {
            STATUSES[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode()));
        }
        for (HttpMethod method : HttpMethod.values())
        {
            C_METHODS.put(method, new PreEncodedHttpField(HttpHeader.C_METHOD, method.asString()));
        }
    }

    private final HpackContext _context;
    private final boolean _debug;
    private int _maxTableCapacity;
    private int _tableCapacity;
    private int _maxHeaderListSize;
    private int _headerListSize;
    private boolean _validateEncoding = true;
    private boolean _maxDynamicTableSizeSent = false;

    public HpackEncoder()
    {
        _context = new HpackContext(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
        _debug = LOG.isDebugEnabled();
        setMaxTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
        setTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
    }

    public int getMaxTableCapacity()
    {
        return _maxTableCapacity;
    }

    /**
     * 

Sets the limit for the capacity of the dynamic header table.

*

This value is set by the remote peer via the * {@code SETTINGS_HEADER_TABLE_SIZE} setting.

* * @param maxTableSizeLimit the limit for capacity of the dynamic header table */ public void setMaxTableCapacity(int maxTableSizeLimit) { _maxTableCapacity = maxTableSizeLimit; } public int getTableCapacity() { return _tableCapacity; } /** *

Sets the capacity of the dynamic header table.

*

The value of the capacity may be changed from {@code 0} * up to {@link #getMaxTableCapacity()}. * An HPACK instruction with the new capacity value will * be sent to the decoder when the next call to * {@link #encode(ByteBuffer, MetaData)} is made.

* * @param tableCapacity the capacity of the dynamic header table */ public void setTableCapacity(int tableCapacity) { if (tableCapacity > getMaxTableCapacity()) throw new IllegalArgumentException("Max table capacity exceeded"); _tableCapacity = tableCapacity; } public int getMaxHeaderListSize() { return _maxHeaderListSize; } public void setMaxHeaderListSize(int maxHeaderListSize) { _maxHeaderListSize = maxHeaderListSize; } public HpackContext getHpackContext() { return _context; } @Deprecated public void setRemoteMaxDynamicTableSize(int maxTableSize) { setTableCapacity(maxTableSize); } @Deprecated public void setLocalMaxDynamicTableSize(int maxTableSizeLimit) { setMaxTableCapacity(maxTableSizeLimit); } public boolean isValidateEncoding() { return _validateEncoding; } public void setValidateEncoding(boolean validateEncoding) { _validateEncoding = validateEncoding; } public void encode(ByteBuffer buffer, MetaData metadata) throws HpackException { try { if (LOG.isDebugEnabled()) LOG.debug(String.format("CtxTbl[%x] encoding", _context.hashCode())); HttpFields fields = metadata.getFields(); // Verify that we can encode without errors. if (isValidateEncoding() && fields != null) { for (HttpField field : fields) { String name = field.getName(); char firstChar = name.charAt(0); if (firstChar <= ' ' || firstChar == ':') throw new HpackException.StreamException("Invalid header name: '%s'", name); } } _headerListSize = 0; int pos = buffer.position(); // If max table size changed, send the correspondent instruction. int tableCapacity = getTableCapacity(); if (!_maxDynamicTableSizeSent || tableCapacity != _context.getMaxDynamicTableSize()) { _maxDynamicTableSizeSent = true; encodeMaxDynamicTableSize(buffer, tableCapacity); } // Add Request/response meta fields if (metadata.isRequest()) { MetaData.Request request = (MetaData.Request)metadata; String method = request.getMethod(); HttpMethod httpMethod = method == null ? null : HttpMethod.fromString(method); HttpField methodField = C_METHODS.get(httpMethod); encode(buffer, methodField == null ? new HttpField(HttpHeader.C_METHOD, method) : methodField); encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority())); boolean isConnect = HttpMethod.CONNECT.is(request.getMethod()); String protocol = request.getProtocol(); if (!isConnect || protocol != null) { String scheme = request.getURI().getScheme(); encode(buffer, HttpScheme.HTTPS.is(scheme) ? C_SCHEME_HTTPS : C_SCHEME_HTTP); encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery())); if (protocol != null) encode(buffer, new HttpField(HttpHeader.C_PROTOCOL, protocol)); } } else if (metadata.isResponse()) { MetaData.Response response = (MetaData.Response)metadata; int code = response.getStatus(); HttpField status = code < STATUSES.length ? STATUSES[code] : null; if (status == null) status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code); encode(buffer, status); } // Remove fields as specified in RFC 7540, 8.1.2.2. if (fields != null) { // Remove the headers specified in the Connection header, // for example: Connection: Close, TE, Upgrade, Custom. Set hopHeaders = null; for (String value : fields.getCSV(HttpHeader.CONNECTION, false)) { if (hopHeaders == null) hopHeaders = new HashSet<>(); hopHeaders.add(StringUtil.asciiToLowerCase(value)); } boolean contentLengthEncoded = false; for (HttpField field : fields) { HttpHeader header = field.getHeader(); if (header != null && IGNORED_HEADERS.contains(header)) continue; if (header == HttpHeader.TE) { if (field.contains("trailers")) encode(buffer, TE_TRAILERS); continue; } String name = field.getLowerCaseName(); if (hopHeaders != null && hopHeaders.contains(name)) continue; if (header == HttpHeader.CONTENT_LENGTH) contentLengthEncoded = true; encode(buffer, field); } if (!contentLengthEncoded) { long contentLength = metadata.getContentLength(); if (contentLength >= 0) encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength))); } } 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)); } catch (HpackException x) { throw x; } catch (Throwable x) { HpackException.SessionException failure = new HpackException.SessionException("Could not hpack encode %s", metadata); failure.initCause(x); throw failure; } } public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxTableSize) { buffer.put((byte)0x20); NBitIntegerEncoder.encode(buffer, 5, maxTableSize); _context.resize(maxTableSize); } public void encode(ByteBuffer buffer, HttpField field) { if (field.getValue() == null) field = new HttpField(field.getHeader(), field.getName(), ""); int fieldSize = field.getName().length() + field.getValue().length(); _headerListSize += fieldSize + 32; String encoding = null; // Is there an index entry for the field? Entry entry = _context.get(field); if (entry != null) { // This is a known indexed field, send as static or dynamic indexed. if (entry.isStatic()) { buffer.put(((StaticEntry)entry).getEncodedField()); if (_debug) encoding = "IdxFieldS1"; } else { int index = _context.index(entry); buffer.put((byte)0x80); NBitIntegerEncoder.encode(buffer, 7, index); if (_debug) encoding = "IdxField" + (entry.isStatic() ? "S" : "") + NBitIntegerEncoder.octetsNeeded(7, index); } } else { // Unknown field entry, so we will have to send literally, but perhaps add an index. final boolean indexed; // Do we know its name? HttpHeader header = field.getHeader(); // Select encoding strategy if (header == null) { // Select encoding strategy for unknown header names Entry name = _context.get(field.getName()); if (field instanceof PreEncodedHttpField) { int i = buffer.position(); ((PreEncodedHttpField)field).putTo(buffer, HttpVersion.HTTP_2); byte b = buffer.get(i); indexed = b < 0 || b >= 0x40; if (_debug) encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; } else if (name == null && fieldSize < _context.getMaxDynamicTableSize()) { // unknown name and value that will fit in dynamic table, so let's index // this just in case it is the first time we have seen a custom name or a // custom field. Unless the name is once only, this is worthwhile indexed = true; encodeName(buffer, (byte)0x40, 6, field.getName(), null); encodeValue(buffer, true, field.getValue()); if (_debug) encoding = "LitHuffNHuffVIdx"; } else { // Known name, but different value. // This is probably a custom field with changing value, so don't index. indexed = false; encodeName(buffer, (byte)0x00, 4, field.getName(), null); encodeValue(buffer, true, field.getValue()); if (_debug) encoding = "LitHuffNHuffV!Idx"; } } else { // Select encoding strategy for known header names Entry name = _context.get(header); if (field instanceof PreEncodedHttpField) { // Preencoded field int i = buffer.position(); ((PreEncodedHttpField)field).putTo(buffer, HttpVersion.HTTP_2); byte b = buffer.get(i); indexed = b < 0 || b >= 0x40; if (_debug) encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; } else if (DO_NOT_INDEX.contains(header)) { // Non indexed field indexed = false; boolean neverIndex = NEVER_INDEX.contains(header); boolean huffman = !DO_NOT_HUFFMAN.contains(header); encodeName(buffer, neverIndex ? (byte)0x10 : (byte)0x00, 4, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); if (_debug) encoding = "Lit" + ((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. indexed = false; encodeName(buffer, (byte)0x00, 4, header.asString(), name); encodeValue(buffer, true, field.getValue()); if (_debug) encoding = "Lit" + ((name == null) ? "HuffN" : "IdxNS" + (1 + NBitIntegerEncoder.octetsNeeded(4, _context.index(name)))) + "HuffV!Idx"; } else { // indexed indexed = true; boolean huffman = !DO_NOT_HUFFMAN.contains(header); encodeName(buffer, (byte)0x40, 6, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); if (_debug) encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(6, _context.index(name))))) + (huffman ? "HuffVIdx" : "LitVIdx"); } } // If we want the field referenced, then we add it to our table and reference set. if (indexed) _context.add(field); } if (_debug) { if (LOG.isDebugEnabled()) LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString(buffer.duplicate().flip())); } } private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Entry entry) { buffer.put(mask); if (entry == null) { // leave name index bits as 0 // Encode the name always with lowercase huffman buffer.put((byte)0x80); NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowerCase(name)); HuffmanEncoder.encodeLowerCase(buffer, name); } else { NBitIntegerEncoder.encode(buffer, bits, _context.index(entry)); } } static void encodeValue(ByteBuffer buffer, boolean huffman, String value) { NBitStringEncoder.encode(buffer, 8, value, huffman); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy