Merge pull request #4116 from eclipse/jetty-9.4.x-4115-drop_http2_pseudo_headers

Fixes #4115 - Drop HTTP/2 pseudo headers.
This commit is contained in:
Simone Bordet 2019-10-10 12:15:58 +02:00 committed by GitHub
commit cd57af4caf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 385 additions and 122 deletions

View File

@ -69,6 +69,11 @@ public class HttpField
return _name;
}
public String getLowerCaseName()
{
return _header != null ? _header.lowerCaseName() : StringUtil.asciiToLowerCase(_name);
}
public String getValue()
{
return _value;

View File

@ -149,6 +149,7 @@ public enum HttpHeader
}
private final String _string;
private final String _lowerCase;
private final byte[] _bytes;
private final byte[] _bytesColonSpace;
private final ByteBuffer _buffer;
@ -156,11 +157,17 @@ public enum HttpHeader
HttpHeader(String s)
{
_string = s;
_lowerCase = StringUtil.asciiToLowerCase(s);
_bytes = StringUtil.getBytes(s);
_bytesColonSpace = StringUtil.getBytes(s + ": ");
_buffer = ByteBuffer.wrap(_bytes);
}
public String lowerCaseName()
{
return _lowerCase;
}
public ByteBuffer toBuffer()
{
return _buffer.asReadOnlyBuffer();

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;
@ -1218,7 +1219,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

@ -261,7 +261,7 @@ public class HpackContext
_dynamicTableSizeInBytes += size;
_dynamicTable.add(entry);
_fieldMap.put(field, entry);
_nameMap.put(StringUtil.asciiToLowerCase(field.getName()), entry);
_nameMap.put(field.getLowerCaseName(), entry);
if (LOG.isDebugEnabled())
LOG.debug(String.format("HdrTbl[%x] added %s", hashCode(), entry));
@ -383,7 +383,7 @@ public class HpackContext
_dynamicTableSizeInBytes -= entry.getSize();
entry._slot = -1;
_fieldMap.remove(entry.getHttpField());
String lc = StringUtil.asciiToLowerCase(entry.getHttpField().getName());
String lc = entry.getHttpField().getLowerCaseName();
if (entry == _nameMap.get(lc))
_nameMap.remove(lc);
}

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
@ -27,6 +28,7 @@ import java.util.Set;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
@ -77,6 +79,9 @@ public class HpackEncoder
private static final EnumSet<HttpHeader> IGNORED_HEADERS = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.KEEP_ALIVE,
HttpHeader.PROXY_CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.UPGRADE);
private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers");
private static final PreEncodedHttpField C_SCHEME_HTTP = new PreEncodedHttpField(HttpHeader.C_SCHEME, "http");
private static final PreEncodedHttpField C_SCHEME_HTTPS = new PreEncodedHttpField(HttpHeader.C_SCHEME, "https");
private static final EnumMap<HttpMethod, PreEncodedHttpField> C_METHODS = new EnumMap<>(HttpMethod.class);
static
{
@ -84,6 +89,10 @@ public class HpackEncoder
{
STATUSES[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode()));
}
for (HttpMethod method : HttpMethod.values())
{
C_METHODS.put(method, new PreEncodedHttpField(HttpHeader.C_METHOD, method.asString()));
}
}
private final HpackContext _context;
@ -92,6 +101,7 @@ public class HpackEncoder
private int _localMaxDynamicTableSize;
private int _maxHeaderListSize;
private int _headerListSize;
private boolean _validateEncoding = true;
public HpackEncoder()
{
@ -142,80 +152,118 @@ 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();
// Verify that we can encode without errors.
if (isValidateEncoding() && fields != null)
{
for (HttpField field : fields)
{
String name = field.getName();
char firstChar = name.charAt(0);
if (firstChar <= ' ' || firstChar == ':')
throw new HpackException.StreamException("Invalid header name: '%s'", name);
}
}
_headerListSize = 0;
int pos = buffer.position();
// 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;
String scheme = request.getURI().getScheme();
encode(buffer, HttpScheme.HTTPS.is(scheme) ? C_SCHEME_HTTPS : C_SCHEME_HTTP);
String method = request.getMethod();
HttpMethod httpMethod = method == null ? null : HttpMethod.fromString(method);
HttpField methodField = C_METHODS.get(httpMethod);
encode(buffer, methodField == null ? new HttpField(HttpHeader.C_METHOD, method) : methodField);
encode(buffer, new HttpField(HttpHeader.C_AUTHORITY, request.getURI().getAuthority()));
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 = field.getLowerCaseName();
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