Fixes #4115 - Drop HTTP/2 pseudo headers.

Invalid HTTP/2 headers are now causing an error rather than being ignored.

HTTP2Flusher now catches HpackException.StreamException and generates a
RST_STREAM frame, rather than just closing the connection.

Modified HpackEncoder to throw HpackException in case of encoding failure.
Introduced HpackEncoder.validateEncoding (defaults true) so validation of
the headers can be disabled (useful for tests).

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2019-09-23 22:56:50 +02:00
parent c19d33dc59
commit 609c144ae0
15 changed files with 364 additions and 120 deletions

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@ -44,7 +45,9 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.parser.RateControl;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
@ -58,8 +61,12 @@ import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
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;
public class HTTP2Test extends AbstractTest
@ -792,6 +799,100 @@ public class HTTP2Test extends AbstractTest
assertTrue(goAwayLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testClientInvalidHeader() throws Exception
{
start(new EmptyHttpServlet());
// A bad header in the request should fail on the client.
Session session = newClient(new Session.Listener.Adapter());
HttpFields requestFields = new HttpFields();
requestFields.put(":custom", "special");
MetaData.Request metaData = newRequest("GET", requestFields);
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(request, promise, new Stream.Listener.Adapter());
ExecutionException x = assertThrows(ExecutionException.class, () -> promise.get(5, TimeUnit.SECONDS));
assertThat(x.getCause(), instanceOf(HpackException.StreamException.class));
}
@Test
public void testServerInvalidHeader() throws Exception
{
start(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
{
response.setHeader(":custom", "special");
}
});
// Good request with bad header in the response.
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request metaData = newRequest("GET", new HttpFields());
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch resetLatch = new CountDownLatch(1);
session.newStream(request, promise, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
resetLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
assertNotNull(stream);
assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testServerInvalidHeaderFlushed() throws Exception
{
CountDownLatch serverFailure = new CountDownLatch(1);
start(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setHeader(":custom", "special");
try
{
response.flushBuffer();
}
catch (IOException x)
{
assertThat(x.getCause(), instanceOf(HpackException.StreamException.class));
serverFailure.countDown();
throw x;
}
}
});
// Good request with bad header in the response.
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request metaData = newRequest("GET", "/flush", new HttpFields());
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch resetLatch = new CountDownLatch(1);
session.newStream(request, promise, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
// Cannot receive a 500 because we force the flush on the server, so
// the response is committed even if the server was not able to write it.
resetLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
assertNotNull(stream);
assertTrue(serverFailure.await(5, TimeUnit.SECONDS));
assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
}
private static void sleep(long time)
{
try

View File

@ -35,6 +35,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
@ -295,7 +296,34 @@ public class TrailersTest extends AbstractTest
}
@Test
public void testRequestTrailerInvalidHpack() throws Exception
public void testRequestTrailerInvalidHpackSent() throws Exception
{
start(new EmptyHttpServlet());
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request request = newRequest("POST", new HttpFields());
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(requestFrame, promise, new Stream.Listener.Adapter());
Stream stream = promise.get(5, TimeUnit.SECONDS);
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), data, false), completable);
CountDownLatch failureLatch = new CountDownLatch(1);
completable.thenRun(() ->
{
// Invalid trailer: cannot contain pseudo headers.
HttpFields trailerFields = new HttpFields();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
stream.headers(trailerFrame, Callback.from(Callback.NOOP::succeeded, x -> failureLatch.countDown()));
});
assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testRequestTrailerInvalidHpackReceived() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new HttpServlet()
@ -341,6 +369,8 @@ public class TrailersTest extends AbstractTest
stream.data(new DataFrame(stream.getId(), data, false), completable);
completable.thenRun(() ->
{
// Disable checks for invalid headers.
((HTTP2Session)session).getGenerator().setValidateHpackEncoding(false);
// Invalid trailer: cannot contain pseudo headers.
HttpFields trailerFields = new HttpFields();
trailerFields.put(HttpHeader.C_METHOD, "GET");

View File

@ -30,6 +30,7 @@ import java.util.Set;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.util.Callback;
@ -207,6 +208,13 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
}
}
}
catch (HpackException.StreamException failure)
{
if (LOG.isDebugEnabled())
LOG.debug("Failure generating " + entry, failure);
entry.failed(failure);
pending.remove();
}
catch (Throwable failure)
{
// Failure to generate the entry is catastrophic.
@ -397,7 +405,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
return 0;
}
protected abstract boolean generate(ByteBufferPool.Lease lease);
protected abstract boolean generate(ByteBufferPool.Lease lease) throws HpackException;
public abstract long onFlushed(long bytes) throws IOException;

View File

@ -49,6 +49,7 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
@ -1219,7 +1220,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
@Override
protected boolean generate(ByteBufferPool.Lease lease)
protected boolean generate(ByteBufferPool.Lease lease) throws HpackException
{
frameBytes = generator.control(lease, frame);
beforeSend();

View File

@ -20,8 +20,11 @@ package org.eclipse.jetty.http2.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
public abstract class FrameGenerator
@ -33,7 +36,7 @@ public abstract class FrameGenerator
this.headerGenerator = headerGenerator;
}
public abstract int generate(ByteBufferPool.Lease lease, Frame frame);
public abstract int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException;
protected ByteBuffer generateHeader(ByteBufferPool.Lease lease, FrameType frameType, int length, int flags, int streamId)
{
@ -44,4 +47,19 @@ public abstract class FrameGenerator
{
return headerGenerator.getMaxFrameSize();
}
protected ByteBuffer encode(HpackEncoder encoder, ByteBufferPool.Lease lease, MetaData metaData, int maxFrameSize) throws HpackException
{
ByteBuffer hpacked = lease.acquire(maxFrameSize, false);
try
{
encoder.encode(hpacked, metaData);
return hpacked;
}
catch (HpackException x)
{
lease.release(hpacked);
throw x;
}
}
}

View File

@ -22,6 +22,7 @@ import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
public class Generator
@ -65,6 +66,11 @@ public class Generator
return byteBufferPool;
}
public void setValidateHpackEncoding(boolean validateEncoding)
{
hpackEncoder.setValidateEncoding(validateEncoding);
}
public void setHeaderTableSize(int headerTableSize)
{
hpackEncoder.setRemoteMaxDynamicTableSize(headerTableSize);
@ -75,7 +81,7 @@ public class Generator
headerGenerator.setMaxFrameSize(maxFrameSize);
}
public int control(ByteBufferPool.Lease lease, Frame frame)
public int control(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
return generators[frame.getType().getType()].generate(lease, frame);
}

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
@ -50,13 +51,13 @@ public class HeadersGenerator extends FrameGenerator
}
@Override
public int generate(ByteBufferPool.Lease lease, Frame frame)
public int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
HeadersFrame headersFrame = (HeadersFrame)frame;
return generateHeaders(lease, headersFrame.getStreamId(), headersFrame.getMetaData(), headersFrame.getPriority(), headersFrame.isEndStream());
}
public int generateHeaders(ByteBufferPool.Lease lease, int streamId, MetaData metaData, PriorityFrame priority, boolean endStream)
public int generateHeaders(ByteBufferPool.Lease lease, int streamId, MetaData metaData, PriorityFrame priority, boolean endStream) throws HpackException
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);
@ -66,10 +67,7 @@ public class HeadersGenerator extends FrameGenerator
if (priority != null)
flags = Flags.PRIORITY;
int maxFrameSize = getMaxFrameSize();
ByteBuffer hpacked = lease.acquire(maxFrameSize, false);
BufferUtil.clearToFill(hpacked);
encoder.encode(hpacked, metaData);
ByteBuffer hpacked = encode(encoder, lease, metaData, getMaxFrameSize());
int hpackedLength = hpacked.position();
BufferUtil.flipToFlush(hpacked, 0);

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
@ -40,13 +41,13 @@ public class PushPromiseGenerator extends FrameGenerator
}
@Override
public int generate(ByteBufferPool.Lease lease, Frame frame)
public int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
PushPromiseFrame pushPromiseFrame = (PushPromiseFrame)frame;
return generatePushPromise(lease, pushPromiseFrame.getStreamId(), pushPromiseFrame.getPromisedStreamId(), pushPromiseFrame.getMetaData());
}
public int generatePushPromise(ByteBufferPool.Lease lease, int streamId, int promisedStreamId, MetaData metaData)
public int generatePushPromise(ByteBufferPool.Lease lease, int streamId, int promisedStreamId, MetaData metaData) throws HpackException
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);
@ -58,9 +59,7 @@ public class PushPromiseGenerator extends FrameGenerator
int extraSpace = 4;
maxFrameSize -= extraSpace;
ByteBuffer hpacked = lease.acquire(maxFrameSize, false);
BufferUtil.clearToFill(hpacked);
encoder.encode(hpacked, metaData);
ByteBuffer hpacked = encode(encoder, lease, metaData, maxFrameSize);
int hpackedLength = hpacked.position();
BufferUtil.flipToFlush(hpacked, 0);

View File

@ -74,7 +74,7 @@ public class FrameFloodTest
}
@Test
public void testInvalidHeadersFrameFlood()
public void testInvalidHeadersFrameFlood() throws Exception
{
// Invalid MetaData (no method, no scheme, etc).
MetaData.Request metadata = new MetaData.Request(null, (String)null, null, null, HttpVersion.HTTP_2, null, -1);

View File

@ -92,6 +92,7 @@ public class HpackEncoder
private int _localMaxDynamicTableSize;
private int _maxHeaderListSize;
private int _headerListSize;
private boolean _validateEncoding = true;
public HpackEncoder()
{
@ -142,80 +143,120 @@ public class HpackEncoder
_localMaxDynamicTableSize = localMaxDynamicTableSize;
}
public void encode(ByteBuffer buffer, MetaData metadata)
public boolean isValidateEncoding()
{
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] encoding", _context.hashCode()));
return _validateEncoding;
}
_headerListSize = 0;
int pos = buffer.position();
public void setValidateEncoding(boolean validateEncoding)
{
_validateEncoding = validateEncoding;
}
// Check the dynamic table sizes!
int maxDynamicTableSize = Math.min(_remoteMaxDynamicTableSize, _localMaxDynamicTableSize);
if (maxDynamicTableSize != _context.getMaxDynamicTableSize())
encodeMaxDynamicTableSize(buffer, maxDynamicTableSize);
// Add Request/response meta fields
if (metadata.isRequest())
public void encode(ByteBuffer buffer, MetaData metadata) throws HpackException
{
try
{
MetaData.Request request = (MetaData.Request)metadata;
// TODO optimise these to avoid HttpField creation
String scheme = request.getURI().getScheme();
encode(buffer, new HttpField(HttpHeader.C_SCHEME, scheme == null ? HttpScheme.HTTP.asString() : scheme));
encode(buffer, new HttpField(HttpHeader.C_METHOD, request.getMethod()));
encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority()));
encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery()));
}
else if (metadata.isResponse())
{
MetaData.Response response = (MetaData.Response)metadata;
int code = response.getStatus();
HttpField status = code < STATUSES.length ? STATUSES[code] : null;
if (status == null)
status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code);
encode(buffer, status);
}
// Remove fields as specified in RFC 7540, 8.1.2.2.
HttpFields fields = metadata.getFields();
if (fields != null)
{
// For example: Connection: Close, TE, Upgrade, Custom.
Set<String> hopHeaders = null;
for (String value : fields.getCSV(HttpHeader.CONNECTION, false))
{
if (hopHeaders == null)
hopHeaders = new HashSet<>();
hopHeaders.add(StringUtil.asciiToLowerCase(value));
}
for (HttpField field : fields)
{
HttpHeader header = field.getHeader();
if (header != null && IGNORED_HEADERS.contains(header))
continue;
if (header == HttpHeader.TE)
{
if (field.contains("trailers"))
encode(buffer, TE_TRAILERS);
continue;
}
if (hopHeaders != null && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName())))
continue;
encode(buffer, field);
}
}
// Check size
if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize)
{
LOG.warn("Header list size too large {} > {} for {}", _headerListSize, _maxHeaderListSize);
if (LOG.isDebugEnabled())
LOG.debug("metadata={}", metadata);
}
LOG.debug(String.format("CtxTbl[%x] encoding", _context.hashCode()));
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos));
HttpFields fields = metadata.getFields();
if (isValidateEncoding())
{
// Verify that we can encode without errors.
if (fields != null)
{
for (HttpField field : fields)
{
String name = field.getName();
String lowName = StringUtil.asciiToLowerCase(name);
char firstChar = lowName.charAt(0);
if (firstChar <= ' ' || firstChar == ':')
throw new HpackException.StreamException("Invalid header name: '%s'", name);
}
}
}
_headerListSize = 0;
int pos = buffer.position();
// Check the dynamic table sizes!
int maxDynamicTableSize = Math.min(_remoteMaxDynamicTableSize, _localMaxDynamicTableSize);
if (maxDynamicTableSize != _context.getMaxDynamicTableSize())
encodeMaxDynamicTableSize(buffer, maxDynamicTableSize);
// Add Request/response meta fields
if (metadata.isRequest())
{
MetaData.Request request = (MetaData.Request)metadata;
// TODO optimise these to avoid HttpField creation
String scheme = request.getURI().getScheme();
encode(buffer, new HttpField(HttpHeader.C_SCHEME, scheme == null ? HttpScheme.HTTP.asString() : scheme));
encode(buffer, new HttpField(HttpHeader.C_METHOD, request.getMethod()));
encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority()));
encode(buffer, new HttpField(HttpHeader.C_PATH, request.getURI().getPathQuery()));
}
else if (metadata.isResponse())
{
MetaData.Response response = (MetaData.Response)metadata;
int code = response.getStatus();
HttpField status = code < STATUSES.length ? STATUSES[code] : null;
if (status == null)
status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code);
encode(buffer, status);
}
// Remove fields as specified in RFC 7540, 8.1.2.2.
if (fields != null)
{
// For example: Connection: Close, TE, Upgrade, Custom.
Set<String> hopHeaders = null;
for (String value : fields.getCSV(HttpHeader.CONNECTION, false))
{
if (hopHeaders == null)
hopHeaders = new HashSet<>();
hopHeaders.add(StringUtil.asciiToLowerCase(value));
}
for (HttpField field : fields)
{
HttpHeader header = field.getHeader();
if (header != null && IGNORED_HEADERS.contains(header))
continue;
if (header == HttpHeader.TE)
{
if (field.contains("trailers"))
encode(buffer, TE_TRAILERS);
continue;
}
String name = StringUtil.asciiToLowerCase(field.getName());
if (hopHeaders != null && hopHeaders.contains(name))
continue;
encode(buffer, field);
}
}
// Check size
if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize)
{
LOG.warn("Header list size too large {} > {} for {}", _headerListSize, _maxHeaderListSize);
if (LOG.isDebugEnabled())
LOG.debug("metadata={}", metadata);
}
if (LOG.isDebugEnabled())
LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos));
}
catch (HpackException x)
{
throw x;
}
catch (Throwable x)
{
HpackException.SessionException failure = new HpackException.SessionException("Could not hpack encode %s", metadata);
failure.initCause(x);
throw failure;
}
}
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)

View File

@ -32,13 +32,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
*
*/
public class HpackEncoderTest
{
@Test
public void testUnknownFieldsContextManagement()
public void testUnknownFieldsContextManagement() throws Exception
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HttpFields fields = new HttpFields();
@ -149,7 +146,7 @@ public class HpackEncoderTest
}
@Test
public void testNeverIndexSetCookie()
public void testNeverIndexSetCookie() throws Exception
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
ByteBuffer buffer = BufferUtil.allocate(4096);
@ -181,7 +178,7 @@ public class HpackEncoderTest
}
@Test
public void testFieldLargerThanTable()
public void testFieldLargerThanTable() throws Exception
{
HttpFields fields = new HttpFields();
@ -199,6 +196,7 @@ public class HpackEncoderTest
BufferUtil.flipToFlush(buffer1, pos);
encoder = new HpackEncoder(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 " +
@ -210,6 +208,7 @@ public class HpackEncoderTest
BufferUtil.flipToFlush(buffer2, pos);
encoder = new HpackEncoder(128);
encoder.setValidateEncoding(false);
fields.add(new HttpField("host", "somehost"));
ByteBuffer buffer = BufferUtil.allocate(4096);
pos = BufferUtil.flipToFill(buffer);
@ -243,7 +242,7 @@ public class HpackEncoderTest
}
@Test
public void testResize()
public void testResize() throws Exception
{
HttpFields fields = new HttpFields();
fields.add("host", "localhost0");

View File

@ -20,7 +20,6 @@ package org.eclipse.jetty.http2.hpack;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.nio.ByteBuffer;
import java.util.Map;
@ -34,6 +33,8 @@ 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 HpackPerfTest
{
int _maxDynamicTableSize = 4 * 1024;
@ -56,28 +57,22 @@ public class HpackPerfTest
@Test
public void simpleTest() throws Exception
{
runStories(_maxDynamicTableSize);
runStories();
}
private void runStories(int maxDynamicTableSize) throws Exception
private void runStories() throws Exception
{
// Find files
File data = MavenTestingUtils.getTestResourceDir("data");
String[] files = data.list(new FilenameFilter()
{
@Override
public boolean accept(File dir, String name)
{
return name.startsWith("story_");
}
});
String[] files = data.list((dir, name) -> name.startsWith("story_"));
assertNotNull(files);
// Parse JSON
Map<String, Object>[] stories = new Map[files.length];
Map[] stories = new Map[files.length];
int i = 0;
for (String story : files)
{
stories[i++] = (Map<String, Object>)JSON.parse(new FileReader(new File(data, story)));
stories[i++] = (Map)JSON.parse(new FileReader(new File(data, story)));
}
ByteBuffer buffer = BufferUtil.allocate(256 * 1024);
@ -93,25 +88,27 @@ public class HpackPerfTest
encodeStories(buffer, stories, "response");
}
private void encodeStories(ByteBuffer buffer, Map<String, Object>[] stories, String type) throws Exception
private void encodeStories(ByteBuffer buffer, Map[] stories, String type) throws Exception
{
for (Map<String, Object> story : stories)
for (Map story : stories)
{
if (type.equals(story.get("context")))
{
HpackEncoder encoder = new HpackEncoder(_maxDynamicTableSize, _maxDynamicTableSize);
encoder.setValidateEncoding(false);
// System.err.println(story);
Object[] cases = (Object[])story.get("cases");
for (Object c : cases)
{
// System.err.println(" "+c);
Object[] headers = (Object[])((Map<String, Object>)c).get("headers");
Object[] headers = (Object[])((Map)c).get("headers");
// System.err.println(" "+headers);
HttpFields fields = new HttpFields();
for (Object header : headers)
{
Map<String, String> h = (Map<String, String>)header;
@SuppressWarnings("unchecked")
Map<String, String> h = (Map)header;
Map.Entry<String, String> e = h.entrySet().iterator().next();
fields.add(e.getKey(), e.getValue());
_unencodedSize += e.getKey().length() + e.getValue().length();

View File

@ -36,6 +36,7 @@ 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 HpackTest
@ -251,6 +252,27 @@ public class HpackTest
assertEquals(trailerValue, output.get(HttpHeader.TRAILER));
}
@Test
public void testColonHeaders() throws Exception
{
HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 16384);
HttpFields input = new HttpFields();
input.put(":status", "200");
input.put(":custom", "special");
ByteBuffer buffer = BufferUtil.allocate(2048);
BufferUtil.clearToFill(buffer);
assertThrows(HpackException.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(HpackException.StreamException.class, () -> decoder.decode(buffer));
}
private void assertMetaDataResponseSame(MetaData.Response expected, MetaData.Response actual)
{
assertThat("Response.status", actual.getStatus(), is(expected.getStatus()));

View File

@ -61,6 +61,7 @@ import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.parser.RateControl;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
@ -74,6 +75,7 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -467,21 +469,35 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
@Override
public void onPreface()
{
// Server's preface.
generator.control(lease, new SettingsFrame(new HashMap<>(), false));
// Reply to client's SETTINGS.
generator.control(lease, new SettingsFrame(new HashMap<>(), true));
writeFrames();
try
{
// Server's preface.
generator.control(lease, new SettingsFrame(new HashMap<>(), false));
// Reply to client's SETTINGS.
generator.control(lease, new SettingsFrame(new HashMap<>(), true));
writeFrames();
}
catch (HpackException x)
{
x.printStackTrace();
}
}
@Override
public void onHeaders(HeadersFrame request)
{
// Response.
MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
HeadersFrame response = new HeadersFrame(request.getStreamId(), metaData, null, true);
generator.control(lease, response);
writeFrames();
try
{
// Response.
MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
HeadersFrame response = new HeadersFrame(request.getStreamId(), metaData, null, true);
generator.control(lease, response);
writeFrames();
}
catch (HpackException x)
{
x.printStackTrace();
}
}
private void writeFrames()
@ -573,6 +589,8 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
// Disable checks for invalid headers.
((HTTP2Session)stream.getSession()).getGenerator().setValidateHpackEncoding(false);
// Produce an invalid HPACK block by adding a request pseudo-header to the response.
HttpFields fields = new HttpFields();
fields.put(":method", "get");
@ -601,6 +619,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
@Disabled
@Test
@Tag("external")
public void testExternalServer() throws Exception
{
HTTP2Client http2Client = new HTTP2Client();

View File

@ -127,11 +127,16 @@ public interface ByteBufferPool
{
ByteBuffer buffer = buffers.get(i);
if (recycles.get(i))
byteBufferPool.release(buffer);
release(buffer);
}
buffers.clear();
recycles.clear();
}
public void release(ByteBuffer buffer)
{
byteBufferPool.release(buffer);
}
}
class Bucket