Remove the QPACK tests which only apply to HTTP/2 HPACK.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2021-02-24 00:40:33 +11:00 committed by Simone Bordet
parent 5958bae765
commit ae4f33ed9e
6 changed files with 1 additions and 1776 deletions

View File

@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SuppressWarnings("PointlessArithmeticExpression")
public class NBitIntegerTest
{

View File

@ -1,448 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.http3.qpack;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http3.qpack.table.Entry;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
*
*/
public class QpackContextTest
{
@Test
public void testStaticName()
{
QpackContext ctx = new QpackContext(4096);
Entry entry = ctx.get(":method");
assertEquals(":method", entry.getHttpField().getName());
assertTrue(entry.isStatic());
assertThat(entry.toString(), Matchers.startsWith("{S,2,:method: "));
}
@Test
public void testEmptyAdd()
{
QpackContext ctx = new QpackContext(0);
HttpField field = new HttpField("foo", "bar");
assertNull(ctx.add(field));
}
@Test
public void testTooBigAdd()
{
QpackContext ctx = new QpackContext(37);
HttpField field = new HttpField("foo", "bar");
assertNull(ctx.add(field));
}
@Test
public void testJustRight()
{
QpackContext ctx = new QpackContext(38);
HttpField field = new HttpField("foo", "bar");
Entry entry = ctx.add(field);
assertNotNull(entry);
assertThat(entry.toString(), Matchers.startsWith("{D,0,foo: bar,"));
}
@Test
public void testEvictOne()
{
QpackContext ctx = new QpackContext(38);
HttpField field0 = new HttpField("foo", "bar");
assertEquals(field0, ctx.add(field0).getHttpField());
assertEquals(field0, ctx.get("foo").getHttpField());
HttpField field1 = new HttpField("xxx", "yyy");
assertEquals(field1, ctx.add(field1).getHttpField());
assertNull(ctx.get(field0));
assertNull(ctx.get("foo"));
assertEquals(field1, ctx.get(field1).getHttpField());
assertEquals(field1, ctx.get("xxx").getHttpField());
}
@Test
public void testEvictNames()
{
QpackContext ctx = new QpackContext(38 * 2);
HttpField[] field =
{
new HttpField("name", "v0"),
new HttpField("name", "v1"),
new HttpField("name", "v2"),
new HttpField("name", "v3"),
new HttpField("name", "v4"),
new HttpField("name", "v5"),
};
Entry[] entry = new Entry[field.length];
// Add 2 name entries to fill table
for (int i = 0; i <= 1; i++)
{
entry[i] = ctx.add(field[i]);
}
// check there is a name reference and it is the most recent added
assertEquals(entry[1], ctx.get("name"));
// Add 1 other entry to table and evict 1
ctx.add(new HttpField("xxx", "yyy"));
// check the name reference has been not been evicted
assertEquals(entry[1], ctx.get("name"));
// Add 1 other entry to table and evict 1
ctx.add(new HttpField("foo", "bar"));
// name is evicted
assertNull(ctx.get("name"));
}
@Test
@SuppressWarnings("ReferenceEquality")
public void testGetAddStatic()
{
QpackContext ctx = new QpackContext(4096);
// Look for the field. Should find static version.
HttpField methodGet = new HttpField(":method", "GET");
assertEquals(methodGet, ctx.get(methodGet).getHttpField());
assertTrue(ctx.get(methodGet).isStatic());
// Add static version to dynamic table
Entry e0 = ctx.add(ctx.get(methodGet).getHttpField());
// Look again and should see dynamic version
assertEquals(methodGet, ctx.get(methodGet).getHttpField());
assertFalse(methodGet == ctx.get(methodGet).getHttpField());
assertFalse(ctx.get(methodGet).isStatic());
// Duplicates allows
Entry e1 = ctx.add(ctx.get(methodGet).getHttpField());
// Look again and should see dynamic version
assertEquals(methodGet, ctx.get(methodGet).getHttpField());
assertFalse(methodGet == ctx.get(methodGet).getHttpField());
assertFalse(ctx.get(methodGet).isStatic());
assertFalse(e0 == e1);
}
@Test
public void testGetAddStaticName()
{
QpackContext ctx = new QpackContext(4096);
HttpField methodOther = new HttpField(":method", "OTHER");
// Look for the field by name. Should find static version.
assertEquals(":method", ctx.get(":method").getHttpField().getName());
assertTrue(ctx.get(":method").isStatic());
// Add dynamic entry with method
ctx.add(methodOther);
// Look for the field by name. Should find static version.
assertEquals(":method", ctx.get(":method").getHttpField().getName());
assertTrue(ctx.get(":method").isStatic());
}
@Test
public void testIndexes()
{
// Only enough space for 5 entries
QpackContext ctx = new QpackContext(38 * 5);
HttpField methodPost = new HttpField(":method", "POST");
HttpField[] field =
{
new HttpField("fo0", "b0r"),
new HttpField("fo1", "b1r"),
new HttpField("fo2", "b2r"),
new HttpField("fo3", "b3r"),
new HttpField("fo4", "b4r"),
new HttpField("fo5", "b5r"),
new HttpField("fo6", "b6r"),
new HttpField("fo7", "b7r"),
new HttpField("fo8", "b8r"),
new HttpField("fo9", "b9r"),
new HttpField("foA", "bAr"),
};
Entry[] entry = new Entry[100];
// Lookup the index of a static field
assertEquals(0, ctx.getNumEntries());
assertEquals(":authority", ctx.get(1).getHttpField().getName());
assertEquals(3, ctx.index(ctx.get(methodPost)));
assertEquals(methodPost, ctx.get(3).getHttpField());
assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
assertEquals(null, ctx.get(62));
// Add a single entry
entry[0] = ctx.add(field[0]);
// Check new entry is 62
assertEquals(1, ctx.getNumEntries());
assertEquals(62, ctx.index(entry[0]));
assertEquals(entry[0], ctx.get(62));
// and statics still OK
assertEquals(":authority", ctx.get(1).getHttpField().getName());
assertEquals(3, ctx.index(ctx.get(methodPost)));
assertEquals(methodPost, ctx.get(3).getHttpField());
assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
assertEquals(null, ctx.get(62 + ctx.getNumEntries()));
// Add 4 more entries
for (int i = 1; i <= 4; i++)
{
entry[i] = ctx.add(field[i]);
}
// Check newest entry is at 62 oldest at 66
assertEquals(5, ctx.getNumEntries());
int index = 66;
for (int i = 0; i <= 4; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// and statics still OK
assertEquals(":authority", ctx.get(1).getHttpField().getName());
assertEquals(3, ctx.index(ctx.get(methodPost)));
assertEquals(methodPost, ctx.get(3).getHttpField());
assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
assertEquals(null, ctx.get(62 + ctx.getNumEntries()));
// add 1 more entry and this should cause an eviction!
entry[5] = ctx.add(field[5]);
// Check newest entry is at 1 oldest at 5
index = 66;
for (int i = 1; i <= 5; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// check entry 0 evicted
assertNull(ctx.get(field[0]));
assertEquals(0, ctx.index(entry[0]));
// and statics still OK
assertEquals(":authority", ctx.get(1).getHttpField().getName());
assertEquals(3, ctx.index(ctx.get(methodPost)));
assertEquals(methodPost, ctx.get(3).getHttpField());
assertEquals("www-authenticate", ctx.get(61).getHttpField().getName());
assertEquals(null, ctx.get(62 + ctx.getNumEntries()));
// Add 4 more entries
for (int i = 6; i <= 9; i++)
{
entry[i] = ctx.add(field[i]);
}
// Check newest entry is at 1 oldest at 5
index = 66;
for (int i = 5; i <= 9; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// check entry 0-4 evicted
for (int i = 0; i <= 4; i++)
{
assertNull(ctx.get(field[i]));
assertEquals(0, ctx.index(entry[i]));
}
// Add new entries enough so that array queue will wrap
for (int i = 10; i <= 52; i++)
{
entry[i] = ctx.add(new HttpField("n" + i, "v" + i));
}
index = 66;
for (int i = 48; i <= 52; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
}
@Test
public void testResize()
{
// Only enough space for 5 entries
QpackContext ctx = new QpackContext(38 * 5);
HttpField[] field =
{
new HttpField("fo0", "b0r"),
new HttpField("fo1", "b1r"),
new HttpField("fo2", "b2r"),
new HttpField("fo3", "b3r"),
new HttpField("fo4", "b4r"),
new HttpField("fo5", "b5r"),
new HttpField("fo6", "b6r"),
new HttpField("fo7", "b7r"),
new HttpField("fo8", "b8r"),
new HttpField("fo9", "b9r"),
new HttpField("foA", "bAr"),
};
Entry[] entry = new Entry[field.length];
// Add 5 entries
for (int i = 0; i <= 4; i++)
{
entry[i] = ctx.add(field[i]);
}
assertEquals(5, ctx.getNumEntries());
// check indexes
int index = 66;
for (int i = 0; i <= 4; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// resize so that only 2 entries may be held
ctx.resize(38 * 2);
assertEquals(2, ctx.getNumEntries());
// check indexes
index = 63;
for (int i = 3; i <= 4; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// resize so that 6.5 entries may be held
ctx.resize(38 * 6 + 19);
assertEquals(2, ctx.getNumEntries());
// check indexes
index = 63;
for (int i = 3; i <= 4; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// Add 5 entries
for (int i = 5; i <= 9; i++)
{
entry[i] = ctx.add(field[i]);
}
assertEquals(6, ctx.getNumEntries());
// check indexes
index = 67;
for (int i = 4; i <= 9; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// resize so that only 100 entries may be held
ctx.resize(38 * 100);
assertEquals(6, ctx.getNumEntries());
// check indexes
index = 67;
for (int i = 4; i <= 9; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
// add 50 fields
for (int i = 0; i < 50; i++)
{
ctx.add(new HttpField("n" + i, "v" + i));
}
// check indexes
index = 67 + 50;
for (int i = 4; i <= 9; i++)
{
assertEquals(index, ctx.index(entry[i]));
assertEquals(entry[i], ctx.get(index));
index--;
}
}
@Test
public void testStaticHuffmanValues() throws Exception
{
QpackContext ctx = new QpackContext(4096);
for (int i = 2; i <= 14; i++)
{
Entry entry = ctx.get(i);
assertTrue(entry.isStatic());
ByteBuffer buffer = ByteBuffer.wrap(entry.getStaticHuffmanValue());
int huff = 0xff & buffer.get();
assertTrue((0x80 & huff) == 0x80);
int len = NBitInteger.decode(buffer, 7);
assertEquals(len, buffer.remaining());
String value = Huffman.decode(buffer);
assertEquals(entry.getHttpField().getValue(), value);
}
}
@Test
public void testNameInsensitivity()
{
QpackContext ctx = new QpackContext(4096);
assertEquals("content-length", ctx.get("content-length").getHttpField().getName());
assertEquals("content-length", ctx.get("Content-Length").getHttpField().getName());
assertTrue(ctx.get("Content-Length").isStatic());
assertTrue(ctx.get("Content-Type").isStatic());
ctx.add(new HttpField("Wibble", "Wobble"));
assertEquals("Wibble", ctx.get("wibble").getHttpField().getName());
assertEquals("Wibble", ctx.get("Wibble").getHttpField().getName());
}
}

View File

@ -1,580 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.http3.qpack;
import java.nio.ByteBuffer;
import java.util.Iterator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.qpack.QpackException.CompressionException;
import org.eclipse.jetty.http3.qpack.QpackException.SessionException;
import org.eclipse.jetty.http3.qpack.QpackException.StreamException;
import org.eclipse.jetty.util.TypeUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.containsHeaderValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class QpackDecoderTest
{
/*
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
*/
private final int streamId = -1;
private final DecoderTestHandler handler = new DecoderTestHandler();
@Test
public void testDecodeD3() throws Exception
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
// First request
String encoded = "828684410f7777772e6578616d706c652e636f6d";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
MetaData.Request request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
assertFalse(request.iterator().hasNext());
// Second request
encoded = "828684be58086e6f2d6361636865";
buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
Iterator<HttpField> iterator = request.iterator();
assertTrue(iterator.hasNext());
assertEquals(new HttpField("cache-control", "no-cache"), iterator.next());
assertFalse(iterator.hasNext());
// Third request
encoded = "828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565";
buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTPS.asString(), request.getURI().getScheme());
assertEquals("/index.html", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
iterator = request.iterator();
assertTrue(iterator.hasNext());
assertEquals(new HttpField("custom-key", "custom-value"), iterator.next());
assertFalse(iterator.hasNext());
}
@Test
public void testDecodeD4() throws Exception
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
// First request
String encoded = "828684418cf1e3c2e5f23a6ba0ab90f4ff";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
MetaData.Request request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
assertFalse(request.iterator().hasNext());
// Second request
encoded = "828684be5886a8eb10649cbf";
buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
Iterator<HttpField> iterator = request.iterator();
assertTrue(iterator.hasNext());
assertEquals(new HttpField("cache-control", "no-cache"), iterator.next());
assertFalse(iterator.hasNext());
}
@Test
public void testDecodeWithArrayOffset() throws Exception
{
String value = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "8682418cF1E3C2E5F23a6bA0Ab90F4Ff841f0822426173696320515778685a475270626a70766347567549484e6c633246745a513d3d";
byte[] bytes = TypeUtil.fromHexString(encoded);
byte[] array = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, array, 1, bytes.length);
ByteBuffer buffer = ByteBuffer.wrap(array, 1, bytes.length).slice();
decoder.decode(streamId, buffer);
MetaData.Request request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
assertEquals(1, request.getFields().size());
HttpField field = request.iterator().next();
assertEquals(HttpHeader.AUTHORIZATION, field.getHeader());
assertEquals(value, field.getValue());
}
@Test
public void testDecodeHuffmanWithArrayOffset() throws Exception
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "8286418cf1e3c2e5f23a6ba0ab90f4ff84";
byte[] bytes = TypeUtil.fromHexString(encoded);
byte[] array = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, array, 1, bytes.length);
ByteBuffer buffer = ByteBuffer.wrap(array, 1, bytes.length).slice();
decoder.decode(streamId, buffer);
MetaData.Request request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("www.example.com", request.getURI().getHost());
assertFalse(request.iterator().hasNext());
}
@Test
public void testNghttpx() throws Exception
{
// Response encoded by nghttpx
String encoded = "886196C361Be940b6a65B6850400B8A00571972e080a62D1Bf5f87497cA589D34d1f9a0f0d0234327690Aa69D29aFcA954D3A5358980Ae112e0f7c880aE152A9A74a6bF3";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
decoder.decode(streamId, buffer);
MetaData.Response response = (MetaData.Response)handler.getMetaData();
assertThat(response.getStatus(), is(200));
assertThat(response.getFields().size(), is(6));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.DATE, "Fri, 15 Jul 2016 02:36:20 GMT"));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.CONTENT_TYPE, "text/html"));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.CONTENT_ENCODING, ""));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.CONTENT_LENGTH, "42"));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.SERVER, "nghttpx nghttp2/1.12.0"));
assertThat(response.getFields(), containsHeaderValue(HttpHeader.VIA, "1.1 nghttpx"));
}
@Test
public void testResize() throws Exception
{
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
decoder.decode(streamId, buffer);
MetaData metaData = handler.getMetaData();
assertThat(metaData.getFields().get(HttpHeader.HOST), is("localhost0"));
assertThat(metaData.getFields().get(HttpHeader.COOKIE), is("abcdefghij"));
assertThat(decoder.getQpackContext().getMaxDynamicTableSize(), is(50));
assertThat(decoder.getQpackContext().getNumEntries(), is(1));
}
@Test
public void testBadResize() throws Exception
{
/*
4. Dynamic Table Management
4.2. Maximum Table Size
× 1: Sends a dynamic table size update at the end of header block
-> The endpoint MUST treat this as a decoding error.
Expected: GOAWAY Frame (Error Code: COMPRESSION_ERROR)
Connection closed
*/
String encoded = "203f136687A0E41d139d090760881c6490B2Cd39Ba7f20";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
try
{
decoder.decode(streamId, buffer);
fail();
}
catch (CompressionException e)
{
assertThat(e.getMessage(), Matchers.containsString("Dynamic table resize after fields"));
}
}
@Test
public void testTooBigToIndex() throws Exception
{
String encoded = "3f610f17FfEc02Df3990A190A0D4Ee5b3d2940Ec98Aa4a62D127D29e273a0aA20dEcAa190a503b262d8a2671D4A2672a927aA874988a2471D05510750c951139EdA2452a3a548cAa1aA90bE4B228342864A9E0D450A5474a92992a1aA513395448E3A0Aa17B96cFe3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f3f14E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F3E7Cf9f3e7cF9F353F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F54f";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
QpackDecoder decoder = new QpackDecoder(handler, 128, 8192);
decoder.decode(streamId, buffer);
MetaData metaData = handler.getMetaData();
assertThat(decoder.getQpackContext().getDynamicTableSize(), is(0));
assertThat(metaData.getFields().get("host"), Matchers.startsWith("This is a very large field"));
}
@Test
public void testUnknownIndex() throws Exception
{
String encoded = "BE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
QpackDecoder decoder = new QpackDecoder(handler, 128, 8192);
try
{
decoder.decode(streamId, buffer);
fail();
}
catch (SessionException e)
{
assertThat(e.getMessage(), Matchers.startsWith("Unknown index"));
}
}
/* 8.1.2.1. Pseudo-Header Fields */
@Test
public void test8121PseudoHeaderFields() throws Exception
{
// 1:Sends a HEADERS frame that contains a unknown pseudo-header field
MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(":unknown", "value"));
try
{
mdb.build();
fail();
}
catch (StreamException ex)
{
assertThat(ex.getMessage(), Matchers.containsString("Unknown pseudo header"));
}
// 2: Sends a HEADERS frame that contains the pseudo-header field defined for response
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/path"));
mdb.emit(new HttpField(HttpHeader.C_STATUS, "100"));
try
{
mdb.build();
fail();
}
catch (StreamException ex)
{
assertThat(ex.getMessage(), Matchers.containsString("Request and Response headers"));
}
// 3: Sends a HEADERS frame that contains a pseudo-header field as trailers
// 4: Sends a HEADERS frame that contains a pseudo-header field that appears in a header block after a regular header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/path"));
mdb.emit(new HttpField("Accept", "No Compromise"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost"));
try
{
mdb.build();
fail();
}
catch (StreamException ex)
{
assertThat(ex.getMessage(), Matchers.containsString("Pseudo header :authority after fields"));
}
}
@Test
public void test8122ConnectionSpecificHeaderFields() throws Exception
{
MetaDataBuilder mdb;
// 1: Sends a HEADERS frame that contains the connection-specific header field
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.CONNECTION, "value"));
try
{
mdb.build();
fail();
}
catch (StreamException ex)
{
assertThat(ex.getMessage(), Matchers.containsString("Connection specific field 'Connection'"));
}
// 2: Sends a HEADERS frame that contains the TE header field with any value other than "trailers"
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.TE, "not_trailers"));
try
{
mdb.build();
fail();
}
catch (StreamException ex)
{
assertThat(ex.getMessage(), Matchers.containsString("Unsupported TE value 'not_trailers'"));
}
mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.CONNECTION, "TE"));
mdb.emit(new HttpField(HttpHeader.TE, "trailers"));
assertNotNull(mdb.build());
}
@Test
public void test8123RequestPseudoHeaderFields() throws Exception
{
{
MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/"));
assertThat(mdb.build(), Matchers.instanceOf(MetaData.Request.class));
}
{
// 1: Sends a HEADERS frame with empty ":path" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, ""));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("No Path"));
}
{
// 2: Sends a HEADERS frame that omits ":method" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/"));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("No Method"));
}
{
// 3: Sends a HEADERS frame that omits ":scheme" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/"));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("No Scheme"));
}
{
// 4: Sends a HEADERS frame that omits ":path" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("No Path"));
}
{
// 5: Sends a HEADERS frame with duplicated ":method" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/"));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("Duplicate"));
}
{
// 6: Sends a HEADERS frame with duplicated ":scheme" pseudo-header field
final MetaDataBuilder mdb = new MetaDataBuilder(4096);
mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http"));
mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080"));
mdb.emit(new HttpField(HttpHeader.C_PATH, "/"));
StreamException ex = assertThrows(StreamException.class, mdb::build);
assertThat(ex.getMessage(), Matchers.containsString("Duplicate"));
}
}
@Test
public void testHuffmanEncodedStandard() throws Exception
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "83" + "49509F";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
MetaData.Request request = (MetaData.Request)handler.getMetaData();
assertEquals("GET", request.getMethod());
assertEquals(HttpScheme.HTTP.asString(), request.getURI().getScheme());
assertEquals("/", request.getURI().getPath());
assertEquals("test", request.getURI().getHost());
assertFalse(request.iterator().hasNext());
}
/* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */
@Test
public void testHuffmanEncodedExtraPadding()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "84" + "49509FFF";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
/* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */
@Test
public void testHuffmanEncodedZeroPadding()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "83" + "495090";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Incorrect padding"));
}
/* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */
@Test
public void testHuffmanEncodedWithEOS()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "87" + "497FFFFFFF427F";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("EOS in content"));
}
@Test
public void testHuffmanEncodedOneIncompleteOctet()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "81" + "FE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
@Test
public void testHuffmanEncodedTwoIncompleteOctet()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "82868441" + "82" + "FFFE";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Bad termination"));
}
@Test
public void testZeroLengthName()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "00000130";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
SessionException ex = assertThrows(SessionException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Header size 0"));
}
@Test
public void testZeroLengthValue() throws Exception
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "00016800";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
decoder.decode(streamId, buffer);
MetaData metaData = handler.getMetaData();
assertThat(metaData.getFields().size(), is(1));
assertThat(metaData.getFields().get("h"), is(""));
}
@Test
public void testUpperCaseName()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "0001480130";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Uppercase header"));
}
@Test
public void testWhiteSpaceName()
{
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
String encoded = "0001200130";
ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded));
StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(streamId, buffer));
assertThat(ex.getMessage(), Matchers.containsString("Illegal header"));
}
}

View File

@ -1,310 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.http3.qpack;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.qpack.table.StaticTable;
import org.eclipse.jetty.util.BufferUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class QpackEncoderTest
{
@Test
public void testUnknownFieldsContextManagement() throws Exception
{
QpackEncoder encoder = new QpackEncoder(38 * 5);
HttpFields.Mutable fields = HttpFields.build();
HttpField[] field =
{
new HttpField("fo0", "b0r"),
new HttpField("fo1", "b1r"),
new HttpField("fo2", "b2r"),
new HttpField("fo3", "b3r"),
new HttpField("fo4", "b4r"),
new HttpField("fo5", "b5r"),
new HttpField("fo6", "b6r"),
new HttpField("fo7", "b7r"),
new HttpField("fo8", "b8r"),
new HttpField("fo9", "b9r"),
new HttpField("foA", "bAr"),
};
// Add 4 entries
for (int i = 0; i <= 3; i++)
{
fields.add(field[i]);
}
// encode them
ByteBuffer buffer = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, pos);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// All are in the dynamic table
assertEquals(4, encoder.getQpackContext().getNumEntries());
// encode exact same fields again!
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// All are in the dynamic table
assertEquals(4, encoder.getQpackContext().getNumEntries());
// Add 4 more fields
for (int i = 4; i <= 7; i++)
{
fields.add(field[i]);
}
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// max dynamic table size reached
assertEquals(5, encoder.getQpackContext().getNumEntries());
// remove some fields
for (int i = 0; i <= 7; i += 2)
{
fields.remove(field[i].getName());
}
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// max dynamic table size reached
assertEquals(5, encoder.getQpackContext().getNumEntries());
// remove another fields
fields.remove(field[1].getName());
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// max dynamic table size reached
assertEquals(5, encoder.getQpackContext().getNumEntries());
// re add the field
fields.add(field[1]);
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// max dynamic table size reached
assertEquals(5, encoder.getQpackContext().getNumEntries());
}
@Test
public void testLargeFieldsNotIndexed()
{
QpackEncoder encoder = new QpackEncoder(38 * 5);
QpackContext ctx = encoder.getQpackContext();
ByteBuffer buffer = BufferUtil.allocate(4096);
// Index little fields
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField("Name", "Value"));
BufferUtil.flipToFlush(buffer, pos);
int dynamicTableSize = ctx.getDynamicTableSize();
assertThat(dynamicTableSize, Matchers.greaterThan(0));
// Do not index big field
StringBuilder largeName = new StringBuilder("largeName-");
String filler = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
while (largeName.length() < ctx.getMaxDynamicTableSize())
largeName.append(filler, 0, Math.min(filler.length(), ctx.getMaxDynamicTableSize() - largeName.length()));
pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(largeName.toString(), "Value"));
BufferUtil.flipToFlush(buffer, pos);
assertThat(ctx.getDynamicTableSize(), Matchers.is(dynamicTableSize));
}
@Test
public void testIndexContentLength()
{
QpackEncoder encoder = new QpackEncoder(38 * 5);
QpackContext ctx = encoder.getQpackContext();
ByteBuffer buffer = BufferUtil.allocate(4096);
// Index zero content length
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, "0"));
BufferUtil.flipToFlush(buffer, pos);
int dynamicTableSize = ctx.getDynamicTableSize();
assertThat(dynamicTableSize, Matchers.greaterThan(0));
// Do not index non zero content length
pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, "42"));
BufferUtil.flipToFlush(buffer, pos);
assertThat(ctx.getDynamicTableSize(), Matchers.is(dynamicTableSize));
}
@Test
public void testNeverIndexSetCookie() throws Exception
{
QpackEncoder encoder = new QpackEncoder(38 * 5);
ByteBuffer buffer = BufferUtil.allocate(4096);
HttpFields.Mutable fields = HttpFields.build()
.put("set-cookie", "some cookie value");
// encode
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// empty dynamic table
assertEquals(0, encoder.getQpackContext().getNumEntries());
// encode again
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// empty dynamic table
assertEquals(0, encoder.getQpackContext().getNumEntries());
}
@Test
public void testFieldLargerThanTable() throws Exception
{
HttpFields.Mutable fields = HttpFields.build();
QpackEncoder encoder = new QpackEncoder(128);
ByteBuffer buffer0 = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer0);
encoder.encode(buffer0, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer0, pos);
encoder = new QpackEncoder(128);
fields.add(new HttpField("user-agent", "jetty/test"));
ByteBuffer buffer1 = BufferUtil.allocate(4096);
pos = BufferUtil.flipToFill(buffer1);
encoder.encode(buffer1, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer1, pos);
encoder = new QpackEncoder(128);
encoder.setValidateEncoding(false);
fields.add(new HttpField(":path",
"This is a very large field, whose size is larger than the dynamic table so it should not be indexed as it will not fit in the table ever!" +
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX " +
"YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY " +
"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ "));
ByteBuffer buffer2 = BufferUtil.allocate(4096);
pos = BufferUtil.flipToFill(buffer2);
encoder.encode(buffer2, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer2, pos);
encoder = new QpackEncoder(128);
encoder.setValidateEncoding(false);
fields.add(new HttpField("host", "somehost"));
ByteBuffer buffer = BufferUtil.allocate(4096);
pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, pos);
//System.err.println(BufferUtil.toHexString(buffer0));
//System.err.println(BufferUtil.toHexString(buffer1));
//System.err.println(BufferUtil.toHexString(buffer2));
//System.err.println(BufferUtil.toHexString(buffer));
// something was encoded!
assertThat(buffer.remaining(), Matchers.greaterThan(0));
// check first field is static index name and dynamic index body
assertThat((buffer.get(buffer0.remaining()) & 0xFF) >> 6, equalTo(1));
// check first field is static index name and literal body
assertThat((buffer.get(buffer1.remaining()) & 0xFF) >> 4, equalTo(0));
// check first field is static index name and dynamic index body
assertThat((buffer.get(buffer2.remaining()) & 0xFF) >> 6, equalTo(1));
// Only first and third fields are put in the table
QpackContext context = encoder.getQpackContext();
assertThat(context.getNumEntries(), equalTo(2));
assertThat(context.get(StaticTable.STATIC_SIZE + 1).getHttpField().getName(), equalTo("host"));
assertThat(context.get(StaticTable.STATIC_SIZE + 2).getHttpField().getName(), equalTo("user-agent"));
assertThat(context.getDynamicTableSize(), equalTo(
context.get(StaticTable.STATIC_SIZE + 1).getSize() + context.get(StaticTable.STATIC_SIZE + 2).getSize()));
}
@Test
public void testResize() throws Exception
{
HttpFields fields = HttpFields.build()
.add("host", "localhost0")
.add("cookie", "abcdefghij");
QpackEncoder encoder = new QpackEncoder(4096);
ByteBuffer buffer = BufferUtil.allocate(4096);
int pos = BufferUtil.flipToFill(buffer);
encoder.encodeMaxDynamicTableSize(buffer, 0);
encoder.setRemoteMaxDynamicTableSize(50);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, pos);
QpackContext context = encoder.getQpackContext();
assertThat(context.getMaxDynamicTableSize(), Matchers.is(50));
assertThat(context.getNumEntries(), Matchers.is(1));
}
}

View File

@ -1,123 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.http3.qpack;
import java.io.File;
import java.io.FileReader;
import java.nio.ByteBuffer;
import java.util.Map;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ajax.JSON;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class QpackPerfTest
{
int _maxDynamicTableSize = 4 * 1024;
int _unencodedSize;
int _encodedSize;
@BeforeEach
public void before()
{
_unencodedSize = 0;
_encodedSize = 0;
}
@AfterEach
public void after()
{
System.err.printf("dynamictable=%d unencoded=%d encoded=%d p=%3.1f%%%n", _maxDynamicTableSize, _unencodedSize, _encodedSize, 100.0 * _encodedSize / _unencodedSize);
}
@Test
public void simpleTest() throws Exception
{
runStories();
}
private void runStories() throws Exception
{
// Find files
File data = MavenTestingUtils.getTestResourceDir("data");
String[] files = data.list((dir, name) -> name.startsWith("story_"));
assertNotNull(files);
// Parse JSON
@SuppressWarnings("unchecked")
Map<String, Object>[] stories = new Map[files.length];
int i = 0;
for (String file : files)
{
@SuppressWarnings("unchecked")
var story = (Map<String, Object>)new JSON().fromJSON(new FileReader(new File(data, file)));
stories[i++] = story;
}
ByteBuffer buffer = BufferUtil.allocate(256 * 1024);
// Encode all the requests
encodeStories(buffer, stories, "request");
// clear table
BufferUtil.clearToFill(buffer);
BufferUtil.flipToFlush(buffer, 0);
// Encode all the responses
encodeStories(buffer, stories, "response");
}
private void encodeStories(ByteBuffer buffer, Map<String, Object>[] stories, String type) throws Exception
{
for (Map<String, Object> story : stories)
{
if (type.equals(story.get("context")))
{
QpackEncoder encoder = new QpackEncoder(_maxDynamicTableSize, _maxDynamicTableSize);
encoder.setValidateEncoding(false);
Object[] cases = (Object[])story.get("cases");
for (Object c : cases)
{
@SuppressWarnings("unchecked")
var kase = (Map<String, Object>)c;
Object[] headers = (Object[])kase.get("headers");
// System.err.println(" "+headers);
HttpFields.Mutable fields = HttpFields.build();
for (Object header : headers)
{
@SuppressWarnings("unchecked")
var h = (Map<String, String>)header;
Map.Entry<String, String> e = h.entrySet().iterator().next();
fields.add(e.getKey(), e.getValue());
_unencodedSize += e.getKey().length() + e.getValue().length();
}
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, fields));
BufferUtil.flipToFlush(buffer, 0);
_encodedSize += buffer.remaining();
}
}
}
}
}

View File

@ -1,315 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2021 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.http3.qpack;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MetaData.Response;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http3.qpack.table.DynamicTable;
import org.eclipse.jetty.util.BufferUtil;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class QpackTest
{
static final HttpField ServerJetty = new PreEncodedHttpField(HttpHeader.SERVER, "jetty");
static final HttpField XPowerJetty = new PreEncodedHttpField(HttpHeader.X_POWERED_BY, "jetty");
static final HttpField Date = new PreEncodedHttpField(HttpHeader.DATE, DateGenerator.formatDate(TimeUnit.NANOSECONDS.toMillis(System.nanoTime())));
private final int streamId = -1;
private final DecoderTestHandler handler = new DecoderTestHandler();
@Test
public void encodeDecodeResponseTest() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
HttpFields.Mutable fields0 = HttpFields.build()
.add(HttpHeader.CONTENT_TYPE, "text/html")
.add(HttpHeader.CONTENT_LENGTH, "1024")
.add(new HttpField(HttpHeader.CONTENT_ENCODING, (String)null))
.add(ServerJetty)
.add(XPowerJetty)
.add(Date)
.add(HttpHeader.SET_COOKIE, "abcdefghijklmnopqrstuvwxyz")
.add("custom-key", "custom-value");
Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
Response decoded0 = (Response)handler.getMetaData();
Response nullToEmpty = new MetaData.Response(HttpVersion.HTTP_2, 200,
fields0.put(new HttpField(HttpHeader.CONTENT_ENCODING, "")));
assertMetaDataResponseSame(nullToEmpty, decoded0);
// Same again?
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
Response decoded0b = (Response)handler.getMetaData();
assertMetaDataResponseSame(nullToEmpty, decoded0b);
HttpFields.Mutable fields1 = HttpFields.build()
.add(HttpHeader.CONTENT_TYPE, "text/plain")
.add(HttpHeader.CONTENT_LENGTH, "1234")
.add(HttpHeader.CONTENT_ENCODING, " ")
.add(ServerJetty)
.add(XPowerJetty)
.add(Date)
.add("Custom-Key", "Other-Value");
Response original1 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields1);
// Same again?
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original1);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
Response decoded1 = (Response)handler.getMetaData();
assertMetaDataResponseSame(original1, decoded1);
assertEquals("custom-key", decoded1.getFields().getField("Custom-Key").getName());
}
@Test
public void encodeDecodeTooLargeTest() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 164);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
HttpFields fields0 = HttpFields.build()
.add("1234567890", "1234567890123456789012345678901234567890")
.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR");
MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
MetaData decoded0 = handler.getMetaData();
assertMetaDataSame(original0, decoded0);
HttpFields fields1 = HttpFields.build()
.add("1234567890", "1234567890123456789012345678901234567890")
.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR")
.add("x", "y");
MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original1);
BufferUtil.flipToFlush(buffer, 0);
try
{
decoder.decode(streamId, buffer);
fail();
}
catch (QpackException.SessionException e)
{
assertThat(e.getMessage(), containsString("Header too large"));
}
}
@Test
public void encodeDecodeNonAscii() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 8192);
ByteBuffer buffer = BufferUtil.allocate(16 * 1024);
HttpFields fields0 = HttpFields.build()
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
.add("Cookie", "[\uD842\uDF9F]")
.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);
decoder.decode(streamId, buffer);
Response decoded0 = (Response)handler.getMetaData();
assertMetaDataSame(original0, decoded0);
}
@Test
public void evictReferencedFieldTest() throws Exception
{
QpackEncoder encoder = new QpackEncoder(200, 200);
QpackDecoder decoder = new QpackDecoder(handler, 200, 1024);
ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024);
String longEnoughToBeEvicted = "012345678901234567890123456789012345678901234567890";
HttpFields fields0 = HttpFields.build()
.add(longEnoughToBeEvicted, "value")
.add("foo", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
MetaData decoded0 = handler.getMetaData();
assertEquals(2, encoder.getQpackContext().getNumEntries());
assertEquals(2, decoder.getQpackContext().getNumEntries());
assertEquals(longEnoughToBeEvicted, encoder.getQpackContext().get(DynamicTable.FIRST_INDEX + 1).getHttpField().getName());
assertEquals("foo", encoder.getQpackContext().get(DynamicTable.FIRST_INDEX).getHttpField().getName());
assertMetaDataSame(original0, decoded0);
HttpFields fields1 = HttpFields.build()
.add(longEnoughToBeEvicted, "other_value")
.add("x", "y");
MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original1);
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
MetaData decoded1 = handler.getMetaData();
assertMetaDataSame(original1, decoded1);
assertEquals(2, encoder.getQpackContext().getNumEntries());
assertEquals(2, decoder.getQpackContext().getNumEntries());
assertEquals("x", encoder.getQpackContext().get(DynamicTable.FIRST_INDEX).getHttpField().getName());
assertEquals("foo", encoder.getQpackContext().get(DynamicTable.FIRST_INDEX + 1).getHttpField().getName());
}
@Test
public void testHopHeadersAreRemoved() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 16384);
HttpFields input = HttpFields.build()
.add(HttpHeader.ACCEPT, "*")
.add(HttpHeader.CONNECTION, "TE, Upgrade, Custom")
.add("Custom", "Pizza")
.add(HttpHeader.KEEP_ALIVE, "true")
.add(HttpHeader.PROXY_CONNECTION, "foo")
.add(HttpHeader.TE, "1234567890abcdef")
.add(HttpHeader.TRANSFER_ENCODING, "chunked")
.add(HttpHeader.UPGRADE, "gold");
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input));
BufferUtil.flipToFlush(buffer, 0);
decoder.decode(streamId, buffer);
MetaData metaData = handler.getMetaData();
HttpFields output = metaData.getFields();
assertEquals(1, output.size());
assertEquals("*", output.get(HttpHeader.ACCEPT));
}
@Test
public void testTETrailers() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 16384);
String teValue = "trailers";
String trailerValue = "Custom";
HttpFields input = HttpFields.build()
.add(HttpHeader.CONNECTION, "TE")
.add(HttpHeader.TE, teValue)
.add(HttpHeader.TRAILER, trailerValue);
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input));
BufferUtil.flipToFlush(buffer, 0);
MetaData metaData = handler.getMetaData();
HttpFields output = metaData.getFields();
assertEquals(2, output.size());
assertEquals(teValue, output.get(HttpHeader.TE));
assertEquals(trailerValue, output.get(HttpHeader.TRAILER));
}
@Test
public void testColonHeaders() throws Exception
{
QpackEncoder encoder = new QpackEncoder();
QpackDecoder decoder = new QpackDecoder(handler, 4096, 16384);
HttpFields input = HttpFields.build()
.add(":status", "200")
.add(":custom", "special");
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
assertThrows(QpackException.StreamException.class, () -> encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input)));
encoder.setValidateEncoding(false);
encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input));
BufferUtil.flipToFlush(buffer, 0);
assertThrows(QpackException.StreamException.class, () -> decoder.decode(streamId, buffer));
}
private void assertMetaDataResponseSame(MetaData.Response expected, MetaData.Response actual)
{
assertThat("Response.status", actual.getStatus(), is(expected.getStatus()));
assertThat("Response.reason", actual.getReason(), is(expected.getReason()));
assertMetaDataSame(expected, actual);
}
private void assertMetaDataSame(MetaData expected, MetaData actual)
{
assertThat("Metadata.contentLength", actual.getContentLength(), is(expected.getContentLength()));
assertThat("Metadata.version" + ".version", actual.getHttpVersion(), is(expected.getHttpVersion()));
assertHttpFieldsSame(expected.getFields(), actual.getFields());
}
private void assertHttpFieldsSame(HttpFields expected, HttpFields actual)
{
assertThat("metaData.fields.size", actual.size(), is(expected.size()));
for (HttpField actualField : actual)
{
if ("DATE".equalsIgnoreCase(actualField.getName()))
{
// skip comparison on Date, as these values can often differ by 1 second
// during testing.
continue;
}
assertThat("metaData.fields.contains(" + actualField + ")", expected.contains(actualField), is(true));
}
}
}