Hpack.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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.
 */

package io.undertow.protocols.http2;

import java.nio.ByteBuffer;

import io.undertow.UndertowMessages;
import io.undertow.util.HttpString;

/**
 * @author Stuart Douglas
 */
final class Hpack {

    private static final byte LOWER_DIFF = 'a' - 'A';
    static final int DEFAULT_TABLE_SIZE = 4096;
    private static final int MAX_INTEGER_OCTETS = 8; //not sure what a good value for this is, but the spec says we need to provide an upper bound

    /**
     * table that contains powers of two,
     * used as both bitmask and to quickly calculate 2^n
     */
    private static final int[] PREFIX_TABLE;


    static final HeaderField[] STATIC_TABLE;
    static final int STATIC_TABLE_LENGTH;

    static {
        PREFIX_TABLE = new int[32];
        for (int i = 0; i < 32; ++i) {
            int n = 0;
            for (int j = 0; j < i; ++j) {
                n = n << 1;
                n |= 1;
            }
            PREFIX_TABLE[i] = n;
        }

        HeaderField[] fields = new HeaderField[62];
        //note that zero is not used
        fields[1] = new HeaderField(new HttpString(":authority"), null);
        fields[2] = new HeaderField(new HttpString(":method"), "GET");
        fields[3] = new HeaderField(new HttpString(":method"), "POST");
        fields[4] = new HeaderField(new HttpString(":path"), "/");
        fields[5] = new HeaderField(new HttpString(":path"), "/index.html");
        fields[6] = new HeaderField(new HttpString(":scheme"), "http");
        fields[7] = new HeaderField(new HttpString(":scheme"), "https");
        fields[8] = new HeaderField(new HttpString(":status"), "200");
        fields[9] = new HeaderField(new HttpString(":status"), "204");
        fields[10] = new HeaderField(new HttpString(":status"), "206");
        fields[11] = new HeaderField(new HttpString(":status"), "304");
        fields[12] = new HeaderField(new HttpString(":status"), "400");
        fields[13] = new HeaderField(new HttpString(":status"), "404");
        fields[14] = new HeaderField(new HttpString(":status"), "500");
        fields[15] = new HeaderField(new HttpString("accept-charset"), null);
        fields[16] = new HeaderField(new HttpString("accept-encoding"), "gzip, deflate");
        fields[17] = new HeaderField(new HttpString("accept-language"), null);
        fields[18] = new HeaderField(new HttpString("accept-ranges"), null);
        fields[19] = new HeaderField(new HttpString("accept"), null);
        fields[20] = new HeaderField(new HttpString("access-control-allow-origin"), null);
        fields[21] = new HeaderField(new HttpString("age"), null);
        fields[22] = new HeaderField(new HttpString("allow"), null);
        fields[23] = new HeaderField(new HttpString("authorization"), null);
        fields[24] = new HeaderField(new HttpString("cache-control"), null);
        fields[25] = new HeaderField(new HttpString("content-disposition"), null);
        fields[26] = new HeaderField(new HttpString("content-encoding"), null);
        fields[27] = new HeaderField(new HttpString("content-language"), null);
        fields[28] = new HeaderField(new HttpString("content-length"), null);
        fields[29] = new HeaderField(new HttpString("content-location"), null);
        fields[30] = new HeaderField(new HttpString("content-range"), null);
        fields[31] = new HeaderField(new HttpString("content-type"), null);
        fields[32] = new HeaderField(new HttpString("cookie"), null);
        fields[33] = new HeaderField(new HttpString("date"), null);
        fields[34] = new HeaderField(new HttpString("etag"), null);
        fields[35] = new HeaderField(new HttpString("expect"), null);
        fields[36] = new HeaderField(new HttpString("expires"), null);
        fields[37] = new HeaderField(new HttpString("from"), null);
        fields[38] = new HeaderField(new HttpString("host"), null);
        fields[39] = new HeaderField(new HttpString("if-match"), null);
        fields[40] = new HeaderField(new HttpString("if-modified-since"), null);
        fields[41] = new HeaderField(new HttpString("if-none-match"), null);
        fields[42] = new HeaderField(new HttpString("if-range"), null);
        fields[43] = new HeaderField(new HttpString("if-unmodified-since"), null);
        fields[44] = new HeaderField(new HttpString("last-modified"), null);
        fields[45] = new HeaderField(new HttpString("link"), null);
        fields[46] = new HeaderField(new HttpString("location"), null);
        fields[47] = new HeaderField(new HttpString("max-forwards"), null);
        fields[48] = new HeaderField(new HttpString("proxy-authenticate"), null);
        fields[49] = new HeaderField(new HttpString("proxy-authorization"), null);
        fields[50] = new HeaderField(new HttpString("range"), null);
        fields[51] = new HeaderField(new HttpString("referer"), null);
        fields[52] = new HeaderField(new HttpString("refresh"), null);
        fields[53] = new HeaderField(new HttpString("retry-after"), null);
        fields[54] = new HeaderField(new HttpString("server"), null);
        fields[55] = new HeaderField(new HttpString("set-cookie"), null);
        fields[56] = new HeaderField(new HttpString("strict-transport-security"), null);
        fields[57] = new HeaderField(new HttpString("transfer-encoding"), null);
        fields[58] = new HeaderField(new HttpString("user-agent"), null);
        fields[59] = new HeaderField(new HttpString("vary"), null);
        fields[60] = new HeaderField(new HttpString("via"), null);
        fields[61] = new HeaderField(new HttpString("www-authenticate"), null);
        STATIC_TABLE = fields;
        STATIC_TABLE_LENGTH = STATIC_TABLE.length - 1;
    }

    static class HeaderField {
        final HttpString name;
        final String value;
        final int size;

        HeaderField(HttpString name, String value) {
            this.name = name;
            this.value = value;
            if (value != null) {
                this.size = 32 + name.length() + value.length();
            } else {
                this.size = -1;
            }
        }
    }

    /**
     * Decodes an integer in the HPACK prefex format. If the return value is -1
     * it means that there was not enough data in the buffer to complete the decoding
     * sequence.
     * <p>
     * If this method returns -1 then the source buffer will not have been modified.
     *
     * @param source The buffer that contains the integer
     * @param n      The encoding prefix length
     * @return The encoded integer, or -1 if there was not enough data
     */
    static int decodeInteger(ByteBuffer source, int n) throws HpackException {
        if (source.remaining() == 0) {
            return -1;
        }
        if(n >= PREFIX_TABLE.length) {
            throw UndertowMessages.MESSAGES.integerEncodedOverTooManyOctets(MAX_INTEGER_OCTETS);
        }
        int count = 1;
        int sp = source.position();
        int mask = PREFIX_TABLE[n];

        int i = mask & source.get();
        int b;
        if (i < PREFIX_TABLE[n]) {
            return i;
        } else {
            int m = 0;
            do {
                if(count++ > MAX_INTEGER_OCTETS) {
                    throw UndertowMessages.MESSAGES.integerEncodedOverTooManyOctets(MAX_INTEGER_OCTETS);
                }
                if (source.remaining() == 0) {
                    //we have run out of data
                    //reset
                    source.position(sp);
                    return -1;
                }

                if(m >= PREFIX_TABLE.length) {
                    throw UndertowMessages.MESSAGES.integerEncodedOverTooManyOctets(MAX_INTEGER_OCTETS);
                }
                b = source.get();
                i = i + (b & 127) * (PREFIX_TABLE[m] + 1);
                m += 7;
            } while ((b & 128) == 128);
        }
        return i;
    }

    /**
     * Encodes an integer in the HPACK prefix format.
     * <p>
     * This method assumes that the buffer has already had the first 8-n bits filled.
     * As such it will modify the last byte that is already present in the buffer, and
     * potentially add more if required
     *
     * @param source The buffer that contains the integer
     * @param value  The integer to encode
     * @param n      The encoding prefix length
     */
    static void encodeInteger(ByteBuffer source, int value, int n) {
        int twoNminus1 = PREFIX_TABLE[n];
        int pos = source.position() - 1;
        if (value < twoNminus1) {
            source.put(pos, (byte) (source.get(pos) | value));
        } else {
            source.put(pos, (byte) (source.get(pos) | twoNminus1));
            value = value - twoNminus1;
            while (value >= 128) {
                source.put((byte) (value % 128 + 128));
                value = value / 128;
            }
            source.put((byte) value);
        }
    }


    static byte toLower(byte b) {
        if (b >= 'A' && b <= 'Z') {
            return (byte) (b + LOWER_DIFF);
        }
        return b;
    }

    private Hpack() {}

}