Merged branch 'jetty-12.0.x' into 'jetty-12.1.x'.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2024-11-12 11:46:58 +01:00
commit 4f815f6891
No known key found for this signature in database
GPG Key ID: 1677D141BCF3584D
13 changed files with 107 additions and 33 deletions

View File

@ -103,6 +103,7 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport
http2Client.setUseOutputDirectByteBuffers(httpClient.isUseOutputDirectByteBuffers());
http2Client.setConnectBlocking(httpClient.isConnectBlocking());
http2Client.setBindAddress(httpClient.getBindAddress());
http2Client.setMaxRequestHeadersSize(httpClient.getRequestBufferSize());
http2Client.setMaxResponseHeadersSize(httpClient.getMaxResponseHeadersSize());
}

View File

@ -113,6 +113,7 @@ public class HTTP2Client extends ContainerLifeCycle implements AutoCloseable
private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxHeaderBlockFragment = 0;
private int maxRequestHeadersSize = 8 * 1024;
private int maxResponseHeadersSize = 8 * 1024;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout;
@ -357,6 +358,17 @@ public class HTTP2Client extends ContainerLifeCycle implements AutoCloseable
this.maxHeaderBlockFragment = maxHeaderBlockFragment;
}
@ManagedAttribute("The max size of request headers")
public int getMaxRequestHeadersSize()
{
return maxRequestHeadersSize;
}
public void setMaxRequestHeadersSize(int maxRequestHeadersSize)
{
this.maxRequestHeadersSize = maxRequestHeadersSize;
}
@ManagedAttribute("The max size of response headers")
public int getMaxResponseHeadersSize()
{

View File

@ -54,10 +54,11 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
Promise<Session> sessionPromise = (Promise<Session>)context.get(SESSION_PROMISE_CONTEXT_KEY);
Generator generator = new Generator(bufferPool, client.isUseOutputDirectByteBuffers(), client.getMaxHeaderBlockFragment());
generator.getHpackEncoder().setMaxHeaderListSize(client.getMaxRequestHeadersSize());
FlowControlStrategy flowControl = client.getFlowControlStrategyFactory().newFlowControlStrategy();
Parser parser = new Parser(bufferPool, client.getMaxResponseHeadersSize());
parser.setMaxFrameSize(client.getMaxFrameSize());
parser.setMaxSettingsKeys(client.getMaxSettingsKeys());
HTTP2ClientSession session = new HTTP2ClientSession(client.getScheduler(), endPoint, parser, generator, listener, flowControl);

View File

@ -18,6 +18,7 @@ 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.HpackContext;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.RetainableByteBuffer;
@ -49,9 +50,12 @@ public abstract class FrameGenerator
return headerGenerator.isUseDirectByteBuffers();
}
protected RetainableByteBuffer encode(HpackEncoder encoder, MetaData metaData, int maxFrameSize) throws HpackException
protected RetainableByteBuffer encode(HpackEncoder encoder, MetaData metaData) throws HpackException
{
RetainableByteBuffer hpacked = headerGenerator.getByteBufferPool().acquire(maxFrameSize, isUseDirectByteBuffers());
int bufferSize = encoder.getMaxHeaderListSize();
if (bufferSize <= 0)
bufferSize = HpackContext.DEFAULT_MAX_HEADER_LIST_SIZE;
RetainableByteBuffer hpacked = headerGenerator.getByteBufferPool().acquire(bufferSize, isUseDirectByteBuffers());
try
{
ByteBuffer byteBuffer = hpacked.getByteBuffer();

View File

@ -56,9 +56,10 @@ public class HeadersGenerator extends FrameGenerator
throw new IllegalArgumentException("Invalid stream id: " + streamId);
int flags = Flags.NONE;
if (priority != null)
flags = Flags.PRIORITY;
if (endStream)
flags |= Flags.END_STREAM;
// TODO Look for a way of not allocating a large buffer here.
// Possibly the hpack encoder could be changed to take the accumulator, but that is a lot of changes.
@ -66,34 +67,36 @@ public class HeadersGenerator extends FrameGenerator
// So long as the buffer is not sliced into continuations, it at least should be available to aggregate
// subsequent frames into... but likely only a frame header followed by an accumulated data frame.
// It might also be good to be able to split the table into continuation frames as it is generated?
RetainableByteBuffer hpack = encode(encoder, metaData, getMaxFrameSize());
RetainableByteBuffer hpack = encode(encoder, metaData);
BufferUtil.flipToFlush(hpack.getByteBuffer(), 0);
int hpackLength = hpack.remaining();
int maxHeaderBlock = getMaxFrameSize();
if (maxHeaderBlockFragment > 0)
maxHeaderBlock = Math.min(maxHeaderBlock, maxHeaderBlockFragment);
// Split into CONTINUATION frames if necessary.
if (maxHeaderBlockFragment > 0 && hpackLength > maxHeaderBlockFragment)
if (hpackLength > maxHeaderBlock)
{
int start = accumulator.remaining();
if (endStream)
flags |= Flags.END_STREAM;
int length = maxHeaderBlockFragment + (priority == null ? 0 : PriorityFrame.PRIORITY_LENGTH);
int length = maxHeaderBlock + (priority == null ? 0 : PriorityFrame.PRIORITY_LENGTH);
// generate first fragment with as HEADERS with possible priority
// Generate HEADERS frame with possible PRIORITY frame.
generateHeader(accumulator, FrameType.HEADERS, length, flags, streamId);
generatePriority(accumulator, priority);
accumulator.add(hpack.slice(maxHeaderBlockFragment));
hpack.skip(maxHeaderBlockFragment);
accumulator.add(hpack.slice(maxHeaderBlock));
hpack.skip(maxHeaderBlock);
// generate continuation frames that are not the last
while (hpack.remaining() > maxHeaderBlockFragment)
// Generate CONTINUATION frames that are not the last.
while (hpack.remaining() > maxHeaderBlock)
{
generateHeader(accumulator, FrameType.CONTINUATION, maxHeaderBlockFragment, Flags.NONE, streamId);
accumulator.add(hpack.slice(maxHeaderBlockFragment));
hpack.skip(maxHeaderBlockFragment);
generateHeader(accumulator, FrameType.CONTINUATION, maxHeaderBlock, Flags.NONE, streamId);
accumulator.add(hpack.slice(maxHeaderBlock));
hpack.skip(maxHeaderBlock);
}
// generate the last continuation frame
// Generate the last CONTINUATION frame.
generateHeader(accumulator, FrameType.CONTINUATION, hpack.remaining(), Flags.END_HEADERS, streamId);
accumulator.add(hpack);
@ -102,8 +105,6 @@ public class HeadersGenerator extends FrameGenerator
else
{
flags |= Flags.END_HEADERS;
if (endStream)
flags |= Flags.END_STREAM;
int length = hpackLength + (priority == null ? 0 : PriorityFrame.PRIORITY_LENGTH);
generateHeader(accumulator, FrameType.HEADERS, length, flags, streamId);

View File

@ -49,17 +49,17 @@ public class PushPromiseGenerator extends FrameGenerator
if (promisedStreamId < 0)
throw new IllegalArgumentException("Invalid promised stream id: " + promisedStreamId);
int maxFrameSize = getMaxFrameSize();
// The promised streamId space.
int extraSpace = 4;
maxFrameSize -= extraSpace;
RetainableByteBuffer hpack = encode(encoder, metaData, maxFrameSize);
RetainableByteBuffer hpack = encode(encoder, metaData);
ByteBuffer hpackByteBuffer = hpack.getByteBuffer();
int hpackLength = hpackByteBuffer.position();
BufferUtil.flipToFlush(hpackByteBuffer, 0);
int hpackLength = hpackByteBuffer.remaining();
int length = hpackLength + extraSpace;
// No support for splitting in CONTINUATION frames,
// also PushPromiseBodyParser does not support it.
// The promised streamId length.
int promisedStreamIdLength = 4;
int length = hpackLength + promisedStreamIdLength;
int flags = Flags.END_HEADERS;
generateHeader(accumulator, FrameType.PUSH_PROMISE, length, flags, streamId);

View File

@ -209,6 +209,10 @@ public class SettingsBodyParser extends BodyParser
if (maxFrameSize != null && (maxFrameSize < Frame.DEFAULT_MAX_SIZE || maxFrameSize > Frame.MAX_MAX_SIZE))
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size");
Integer maxHeaderListSize = settings.get(SettingsFrame.MAX_HEADER_LIST_SIZE);
if (maxHeaderListSize != null && maxHeaderListSize <= 0)
return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_header_list_size");
SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK));
return onSettings(buffer, frame);
}

View File

@ -116,6 +116,7 @@ public class HpackContext
private static final StaticEntry[] __staticTable = new StaticEntry[STATIC_TABLE.length];
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
public static final int DEFAULT_MAX_TABLE_CAPACITY = 4096;
public static final int DEFAULT_MAX_HEADER_LIST_SIZE = 4096;
static
{

View File

@ -107,6 +107,7 @@ public class HpackEncoder
_debug = LOG.isDebugEnabled();
setMaxTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
setTableCapacity(HpackContext.DEFAULT_MAX_TABLE_CAPACITY);
setMaxHeaderListSize(HpackContext.DEFAULT_MAX_HEADER_LIST_SIZE);
}
public int getMaxTableCapacity()

View File

@ -300,10 +300,13 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
int maxTableSize = getMaxDecoderTableCapacity();
if (maxTableSize != HpackContext.DEFAULT_MAX_TABLE_CAPACITY)
settings.put(SettingsFrame.HEADER_TABLE_SIZE, maxTableSize);
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, getMaxConcurrentStreams());
int initialStreamRecvWindow = getInitialStreamRecvWindow();
if (initialStreamRecvWindow != FlowControlStrategy.DEFAULT_WINDOW_SIZE)
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, initialStreamRecvWindow);
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, getMaxConcurrentStreams());
int maxFrameSize = getMaxFrameSize();
if (maxFrameSize > Frame.DEFAULT_MAX_SIZE)
settings.put(SettingsFrame.MAX_FRAME_SIZE, maxFrameSize);
int maxHeadersSize = getHttpConfiguration().getRequestHeaderSize();
if (maxHeadersSize > 0)
settings.put(SettingsFrame.MAX_HEADER_LIST_SIZE, maxHeadersSize);
@ -318,10 +321,10 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
Generator generator = new Generator(connector.getByteBufferPool(), isUseOutputDirectByteBuffers(), getMaxHeaderBlockFragment());
generator.getHpackEncoder().setMaxHeaderListSize(getHttpConfiguration().getResponseHeaderSize());
FlowControlStrategy flowControl = getFlowControlStrategyFactory().newFlowControlStrategy();
ServerParser parser = newServerParser(connector, getRateControlFactory().newRateControl(endPoint));
parser.setMaxFrameSize(getMaxFrameSize());
parser.setMaxSettingsKeys(getMaxSettingsKeys());
HTTP2ServerSession session = new HTTP2ServerSession(connector.getScheduler(), endPoint, parser, generator, listener, flowControl);

View File

@ -45,8 +45,10 @@ import org.eclipse.jetty.http2.frames.PushPromiseFrame;
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.server.AbstractHTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
@ -1051,7 +1053,7 @@ public class HTTP2Test extends AbstractTest
.put("custom", value);
MetaData.Request metaData = newRequest("GET", requestFields);
HeadersFrame request = new HeadersFrame(metaData, null, true);
session.newStream(request, new FuturePromise<>(), new Stream.Listener(){});
session.newStream(request, new FuturePromise<>(), new Stream.Listener() {});
// Test failure and close reason on client.
String closeReason = clientCloseReasonFuture.get(5, TimeUnit.SECONDS);
@ -1125,7 +1127,7 @@ public class HTTP2Test extends AbstractTest
Session session = newClientSession(listener);
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame request = new HeadersFrame(metaData, null, true);
session.newStream(request, new FuturePromise<>(), new Stream.Listener(){});
session.newStream(request, new FuturePromise<>(), new Stream.Listener() {});
// Test failure and close reason on server.
String closeReason = serverCloseReasonFuture.get(5, TimeUnit.SECONDS);
@ -1294,6 +1296,48 @@ public class HTTP2Test extends AbstractTest
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testLargeRequestHeaders() throws Exception
{
int maxHeadersSize = 20 * 1024;
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setRequestHeaderSize(2 * maxHeadersSize);
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
callback.succeeded();
return true;
}
}, httpConfig);
connector.getBean(AbstractHTTP2ServerConnectionFactory.class).setMaxFrameSize(17 * 1024);
http2Client.setMaxFrameSize(18 * 1024);
Session session = newClientSession(new Session.Listener() {});
CountDownLatch responseLatch = new CountDownLatch(1);
HttpFields.Mutable headers = HttpFields.build()
// Symbol "<" needs 15 bits to be Huffman encoded,
// while letters/numbers take typically less than
// 8 bits, and here we want to exceed maxHeadersSize.
.put("X-Large", "<".repeat(maxHeadersSize));
MetaData.Request request = newRequest("GET", headers);
session.newStream(new HeadersFrame(request, null, true), new Stream.Listener()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
assertTrue(frame.isEndStream());
MetaData.Response response = (MetaData.Response)frame.getMetaData();
assertEquals(HttpStatus.OK_200, response.getStatus());
responseLatch.countDown();
}
}).get(5, TimeUnit.SECONDS);
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
}
private static void sleep(long time)
{
try

View File

@ -145,6 +145,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
assertEquals(httpClient.getIdleTimeout(), http2Client.getIdleTimeout());
assertEquals(httpClient.isUseInputDirectByteBuffers(), http2Client.isUseInputDirectByteBuffers());
assertEquals(httpClient.isUseOutputDirectByteBuffers(), http2Client.isUseOutputDirectByteBuffers());
assertEquals(httpClient.getRequestBufferSize(), http2Client.getMaxRequestHeadersSize());
assertEquals(httpClient.getMaxResponseHeadersSize(), http2Client.getMaxResponseHeadersSize());
}
assertTrue(http2Client.isStopped());

View File

@ -152,7 +152,8 @@ public class ReverseProxyTest extends AbstractProxyTest
@Override
public boolean handle(Request request, Response response, Callback callback)
{
response.getHeaders().put("X-Large", "A".repeat(maxResponseHeadersSize));
// Use "+" because in HTTP/2 is Huffman encoded in more than 8 bits.
response.getHeaders().put("X-Large", "+".repeat(maxResponseHeadersSize));
// With HTTP/1.1, calling response.write() would fail the Handler callback
// which would trigger ErrorHandler and result in a 500 to the proxy.