Remove the QPACK tests which only apply to HTTP/2 HPACK.
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
5958bae765
commit
ae4f33ed9e
|
@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@SuppressWarnings("PointlessArithmeticExpression")
|
||||
public class NBitIntegerTest
|
||||
{
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue