improve testing of http2 client and refine default config (#10580)

* improve testing of http2 client and refine default config
* improve testing in HTTP2Test for hpack

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan 2023-09-27 09:57:06 +10:00 committed by GitHub
parent cc8f976d0c
commit 467052975e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 4 deletions

View File

@ -113,7 +113,7 @@ public class HTTP2Client extends ContainerLifeCycle
private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY; private int maxDecoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY; private int maxEncoderTableCapacity = HpackContext.DEFAULT_MAX_TABLE_CAPACITY;
private int maxHeaderBlockFragment = 0; private int maxHeaderBlockFragment = 0;
private int maxResponseHeadersSize = -1; private int maxResponseHeadersSize = 8 * 1024;
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F); private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout; private long streamIdleTimeout;
private boolean useInputDirectByteBuffers = true; private boolean useInputDirectByteBuffers = true;

View File

@ -55,11 +55,14 @@ import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Jetty; import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.component.Graceful; import org.eclipse.jetty.util.component.Graceful;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -1004,6 +1007,149 @@ public class HTTP2Test extends AbstractTest
assertFalse(((HTTP2Session)serverSession).getEndPoint().isOpen()); assertFalse(((HTTP2Session)serverSession).getEndPoint().isOpen());
} }
@Test
public void testClientSendsLargeHeader() throws Exception
{
CountDownLatch settingsLatch = new CountDownLatch(2);
CompletableFuture<Throwable> serverFailureFuture = new CompletableFuture<>();
CompletableFuture<String> serverCloseReasonFuture = new CompletableFuture<>();
start(new ServerSessionListener.Adapter()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
settingsLatch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure)
{
serverFailureFuture.complete(failure);
}
@Override
public void onClose(Session session, GoAwayFrame frame)
{
serverCloseReasonFuture.complete(frame.tryConvertPayload());
}
});
CompletableFuture<Throwable> clientFailureFuture = new CompletableFuture<>();
CompletableFuture<String> clientCloseReasonFuture = new CompletableFuture<>();
Session.Listener.Adapter listener = new Session.Listener.Adapter()
{
@Override
public void onSettings(Session session, SettingsFrame frame)
{
settingsLatch.countDown();
}
@Override
public void onFailure(Session session, Throwable failure)
{
clientFailureFuture.complete(failure);
}
@Override
public void onClose(Session session, GoAwayFrame frame)
{
clientCloseReasonFuture.complete(frame.tryConvertPayload());
}
};
HTTP2Session session = (HTTP2Session)newClient(listener);
assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
session.getGenerator().getHpackEncoder().setMaxHeaderListSize(1024 * 1024);
String value = StringUtil.stringFrom("x", 8 * 1024);
HttpFields requestFields = HttpFields.build()
.put("custom", value);
MetaData.Request metaData = newRequest("GET", requestFields);
HeadersFrame request = new HeadersFrame(metaData, null, true);
session.newStream(request, new FuturePromise<>(), new Stream.Listener.Adapter());
// Test failure and close reason on client.
String closeReason = clientCloseReasonFuture.get(5, TimeUnit.SECONDS);
assertThat(closeReason, equalTo("invalid_hpack_block"));
assertNull(clientFailureFuture.getNow(null));
// Test failure and close reason on server.
closeReason = serverCloseReasonFuture.get(5, TimeUnit.SECONDS);
assertThat(closeReason, equalTo("invalid_hpack_block"));
Throwable failure = serverFailureFuture.get(5, TimeUnit.SECONDS);
assertThat(failure, instanceOf(IOException.class));
assertThat(failure.getMessage(), containsString("invalid_hpack_block"));
}
@Test
public void testServerSendsLargeHeader() throws Exception
{
CompletableFuture<Throwable> serverFailureFuture = new CompletableFuture<>();
CompletableFuture<String> serverCloseReasonFuture = new CompletableFuture<>();
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
HTTP2Session session = (HTTP2Session)stream.getSession();
session.getGenerator().getHpackEncoder().setMaxHeaderListSize(1024 * 1024);
String value = StringUtil.stringFrom("x", 8 * 1024);
HttpFields fields = HttpFields.build().put("custom", value);
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields);
stream.headers(new HeadersFrame(stream.getId(), response, null, true));
return null;
}
@Override
public void onFailure(Session session, Throwable failure)
{
serverFailureFuture.complete(failure);
}
@Override
public void onClose(Session session, GoAwayFrame frame)
{
serverCloseReasonFuture.complete(frame.tryConvertPayload());
}
});
CompletableFuture<Throwable> clientFailureFuture = new CompletableFuture<>();
CompletableFuture<String> clientCloseReasonFuture = new CompletableFuture<>();
Session.Listener.Adapter listener = new Session.Listener.Adapter()
{
@Override
public void onFailure(Session session, Throwable failure)
{
clientFailureFuture.complete(failure);
}
@Override
public void onClose(Session session, GoAwayFrame frame)
{
clientCloseReasonFuture.complete(frame.tryConvertPayload());
}
};
Session session = newClient(listener);
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame request = new HeadersFrame(metaData, null, true);
session.newStream(request, new FuturePromise<>(), new Stream.Listener.Adapter());
// Test failure and close reason on server.
String closeReason = serverCloseReasonFuture.get(5, TimeUnit.SECONDS);
assertThat(closeReason, equalTo("invalid_hpack_block"));
assertNull(serverFailureFuture.getNow(null));
// Test failure and close reason on client.
closeReason = clientCloseReasonFuture.get(5, TimeUnit.SECONDS);
assertThat(closeReason, equalTo("invalid_hpack_block"));
Throwable failure = clientFailureFuture.get(5, TimeUnit.SECONDS);
assertThat(failure, instanceOf(IOException.class));
assertThat(failure.getMessage(), containsString("invalid_hpack_block"));
}
private static void sleep(long time) private static void sleep(long time)
{ {
try try

View File

@ -45,6 +45,11 @@ public class HeaderBlockParser
this.notifier = notifier; this.notifier = notifier;
} }
public int getMaxHeaderListSize()
{
return hpackDecoder.getMaxHeaderListSize();
}
/** /**
* Parses @{code blockLength} HPACK bytes from the given {@code buffer}. * Parses @{code blockLength} HPACK bytes from the given {@code buffer}.
* *

View File

@ -170,6 +170,10 @@ public class HeadersBodyParser extends BodyParser
{ {
if (hasFlag(Flags.END_HEADERS)) if (hasFlag(Flags.END_HEADERS))
{ {
int maxLength = headerBlockParser.getMaxHeaderListSize();
if (maxLength > 0 && length > maxLength)
return connectionFailure(buffer, ErrorCode.REFUSED_STREAM_ERROR.code, "invalid_headers_frame");
MetaData metaData = headerBlockParser.parse(buffer, length); MetaData metaData = headerBlockParser.parse(buffer, length);
if (metaData == HeaderBlockParser.SESSION_FAILURE) if (metaData == HeaderBlockParser.SESSION_FAILURE)
return false; return false;

View File

@ -119,6 +119,10 @@ public class PushPromiseBodyParser extends BodyParser
} }
case HEADERS: case HEADERS:
{ {
int maxLength = headerBlockParser.getMaxHeaderListSize();
if (maxLength > 0 && length > maxLength)
return connectionFailure(buffer, ErrorCode.REFUSED_STREAM_ERROR.code, "invalid_headers_frame");
MetaData.Request metaData = (MetaData.Request)headerBlockParser.parse(buffer, length); MetaData.Request metaData = (MetaData.Request)headerBlockParser.parse(buffer, length);
if (metaData == HeaderBlockParser.SESSION_FAILURE) if (metaData == HeaderBlockParser.SESSION_FAILURE)
return false; return false;

View File

@ -92,6 +92,11 @@ public class HpackDecoder
setMaxTableCapacity(maxTableSizeLimit); setMaxTableCapacity(maxTableSizeLimit);
} }
public int getMaxHeaderListSize()
{
return _builder.getMaxSize();
}
public void setMaxHeaderListSize(int maxHeaderListSize) public void setMaxHeaderListSize(int maxHeaderListSize)
{ {
_builder.setMaxSize(maxHeaderListSize); _builder.setMaxSize(maxHeaderListSize);

View File

@ -74,7 +74,7 @@ public class MetaDataBuilder
{ {
HttpHeader header = field.getHeader(); HttpHeader header = field.getHeader();
String name = field.getName(); String name = field.getName();
if (name == null || name.length() == 0) if (name == null || name.isEmpty())
throw new SessionException("Header size 0"); throw new SessionException("Header size 0");
String value = field.getValue(); String value = field.getValue();
int fieldSize = name.length() + (value == null ? 0 : value.length()); int fieldSize = name.length() + (value == null ? 0 : value.length());
@ -146,7 +146,7 @@ public class MetaDataBuilder
case C_PATH: case C_PATH:
if (checkPseudoHeader(header, _path)) if (checkPseudoHeader(header, _path))
{ {
if (value != null && value.length() > 0) if (value != null && !value.isEmpty())
_path = value; _path = value;
else else
streamException("No Path"); streamException("No Path");
@ -253,7 +253,7 @@ public class MetaDataBuilder
else else
return new MetaData.Request( return new MetaData.Request(
_method, _method,
_scheme == null ? HttpScheme.HTTP.asString() : _scheme.asString(), _scheme.asString(),
_authority, _authority,
_path, _path,
HttpVersion.HTTP_2, HttpVersion.HTTP_2,