blob: c1281e85ac66880fb8dd053cb2829c6c86a386b4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.hamcrest.Description;
import org.hamcrest.MatcherAssert;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.coyote.http2.HpackEncoder.State;
import org.apache.tomcat.util.http.Method;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.res.StringManager;
public class TestHttp2Limits extends Http2TestBase {
private static final StringManager sm = StringManager.getManager(TestHttp2Limits.class);
@Test
public void testSettingsOverheadLimits() throws Exception {
http2Connect();
String errMsg = sm.getString("upgradeHandler.tooMuchOverhead", "\\p{XDigit}++").replace("[", "\\[");
String overHeadMsgRegx = "0-Goaway-\\[1]-\\[11]-\\[" + errMsg + "]";
for (int i = 0; i < 100; i++) {
try {
sendSettings(0, false);
parser.readFrame();
} catch (IOException ioe) {
// Server closed connection before client has a chance to read
// the Goaway frame. Treat this as a pass.
return;
}
String trace = output.getTrace();
if (trace.equals("0-Settings-Ack\n")) {
// Test continues
output.clearTrace();
} else if (trace.matches(overHeadMsgRegx)) {
// Test passed
return;
} else {
// Test failed
Assert.fail("Unexpected output: " + output.getTrace());
}
Thread.sleep(100);
}
// Test failed
Assert.fail("Connection not closed down");
}
@Test
public void testHeaderLimits1x128() throws Exception {
// Well within limits
doTestHeaderLimits(1, 128, FailureMode.NONE);
}
@Test
public void testHeaderLimits100x32() throws Exception {
// Just within default maxHeaderCount
// Note request has 4 standard headers
doTestHeaderLimits(96, 32, FailureMode.NONE);
}
@Test
public void testHeaderLimits101x32() throws Exception {
// Just above default maxHeaderCount
doTestHeaderLimits(97, 32, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits20x32WithLimit10() throws Exception {
// Check lower count limit is enforced
doTestHeaderLimits(20, 32, -1, 10, Constants.DEFAULT_MAX_HEADER_SIZE, 0, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits8x1144() throws Exception {
// Just within default maxHttpHeaderSize
// per header overhead plus standard 3 headers
doTestHeaderLimits(7, 1144, FailureMode.NONE);
}
@Test
public void testHeaderLimits8x1145() throws Exception {
// Just above default maxHttpHeaderSize
doTestHeaderLimits(7, 1145, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits3x1024WithLimit2048() throws Exception {
// Check lower size limit is enforced
doTestHeaderLimits(3, 1024, -1, Constants.DEFAULT_MAX_HEADER_COUNT, 2 * 1024, 0, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits1x12k() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 12 * 1024, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits1x12kin1kChunks() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 12 * 1024, 1024, FailureMode.STREAM_RESET);
}
@Test
public void testHeaderLimits1x12kin1kChunksThenNewRequest() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 12 * 1024, 1024, FailureMode.STREAM_RESET);
output.clearTrace();
sendSimpleGetRequest(5);
parser.readFrame();
parser.readFrame();
Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace());
}
@Test
public void testHeaderLimits1x32k() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 32 * 1024, FailureMode.CONNECTION_RESET);
}
@Test
public void testHeaderLimits1x32kin1kChunks() throws Exception {
// Bug 60232
// 500ms per frame write delay to give server a chance to process the
// stream reset and the connection reset before the request is fully
// sent.
doTestHeaderLimits(1, 32 * 1024, 1024, 500, FailureMode.STREAM_RESET_THEN_CONNECTION_RESET);
}
@Test
public void testHeaderLimits1x128k() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 128 * 1024, FailureMode.CONNECTION_RESET);
}
@Test
public void testHeaderLimits1x512k() throws Exception {
// Bug 60232
doTestHeaderLimits(1, 512 * 1024, FailureMode.CONNECTION_RESET);
}
@Test
public void testHeaderLimits10x512k() throws Exception {
// Bug 60232
doTestHeaderLimits(10, 512 * 1024, FailureMode.CONNECTION_RESET);
}
private void doTestHeaderLimits(int headerCount, int headerSize, FailureMode failMode) throws Exception {
doTestHeaderLimits(headerCount, headerSize, -1, failMode);
}
private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, FailureMode failMode)
throws Exception {
doTestHeaderLimits(headerCount, headerSize, maxHeaderPayloadSize, 0, failMode);
}
private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, int delayms,
FailureMode failMode) throws Exception {
doTestHeaderLimits(headerCount, headerSize, maxHeaderPayloadSize, Constants.DEFAULT_MAX_HEADER_COUNT,
Constants.DEFAULT_MAX_HEADER_SIZE, delayms, failMode);
}
private void doTestHeaderLimits(int headerCount, int headerSize, int maxHeaderPayloadSize, int maxHeaderCount,
int maxHeaderSize, int delayms, FailureMode failMode) throws Exception {
// Build the custom headers
List<String[]> customHeaders = new ArrayList<>();
StringBuilder headerValue = new StringBuilder(headerSize);
// Does not need to be secure
Random r = new Random();
for (int i = 0; i < headerSize; i++) {
// Random lower case characters
headerValue.append((char) ('a' + r.nextInt(26)));
}
String v = headerValue.toString();
for (int i = 0; i < headerCount; i++) {
customHeaders.add(new String[] { "X-TomcatTest" + i, v });
}
enableHttp2();
http2Protocol.setMaxHeaderCount(maxHeaderCount);
((AbstractHttp11Protocol<?>) http2Protocol.getHttp11Protocol()).setMaxHttpHeaderSize(maxHeaderSize);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
if (maxHeaderPayloadSize == -1) {
maxHeaderPayloadSize = output.getMaxFrameSize();
}
// Build the simple request
byte[] frameHeader = new byte[9];
// Assumes at least one custom header and that all headers are the same
// length. These assumptions are valid for these tests.
ByteBuffer headersPayload = ByteBuffer
.allocate(200 + (int) (customHeaders.size() * customHeaders.iterator().next()[1].length() * 1.2));
populateHeadersPayload(headersPayload, customHeaders, "/simple");
Exception e = null;
try {
int written = 0;
int left = headersPayload.limit() - written;
while (left > 0) {
int thisTime = Math.min(left, maxHeaderPayloadSize);
populateFrameHeader(frameHeader, written, left, thisTime, 3);
writeFrame(frameHeader, headersPayload, headersPayload.limit() - left, thisTime, delayms);
left -= thisTime;
written += thisTime;
}
} catch (IOException ioe) {
e = ioe;
}
switch (failMode) {
case NONE: {
// Expect a normal response
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
Assert.assertNull(e);
break;
}
case STREAM_RESET: {
// Expect a stream reset
parser.readFrame();
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
Assert.assertNull(e);
break;
}
case STREAM_RESET_THEN_CONNECTION_RESET: {
// Expect a stream reset
// On some platform / Connector combinations the TCP connection close
// will be processed before the client gets a chance to read the
// connection close frame which will trigger an
// IOException when we try to read the frame.
try {
parser.readFrame();
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
output.clearTrace();
} catch (IOException ioe) {
// Expected on some platforms
}
}
//$FALL-THROUGH$
case CONNECTION_RESET: {
// This message uses i18n and needs to be used in a regular
// expression (since we don't know the connection ID). Generate the
// string as a regular expression and then replace '[' and ']' with
// the escaped values.
String limitMessage = sm.getString("http2Parser.headerLimitSize", "\\p{XDigit}++", "3");
limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]");
// Connection reset. Connection ID will vary so use a pattern
// On some platform / Connector combinations the TCP connection close
// will be processed before the client gets a chance to read the
// connection close frame which will trigger an
// IOException when we try to read the frame.
// Note: Some platforms will allow the read if if the write fails
// above.
try {
parser.readFrame();
MatcherAssert.assertThat(output.getTrace(),
RegexMatcher.matchesRegex("0-Goaway-\\[1\\]-\\[11\\]-\\[" + limitMessage + "\\]"));
} catch (IOException ignore) {
// Expected on some platforms
}
break;
}
}
}
private void populateHeadersPayload(ByteBuffer headersPayload, List<String[]> customHeaders, String path)
throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.addValue(":method").setString(Method.GET);
headers.addValue(":scheme").setString("http");
headers.addValue(":path").setString(path);
headers.addValue(":authority").setString("localhost:" + getPort());
for (String[] customHeader : customHeaders) {
headers.addValue(customHeader[0]).setString(customHeader[1]);
}
State state = hpackEncoder.encode(headers, headersPayload);
if (state != State.COMPLETE) {
throw new Exception("Unable to build headers");
}
headersPayload.flip();
log.debug("Headers payload generated of size [" + headersPayload.limit() + "]");
}
private void populateFrameHeader(byte[] frameHeader, int written, int left, int thisTime, int streamId)
throws Exception {
ByteUtil.setThreeBytes(frameHeader, 0, thisTime);
if (written == 0) {
frameHeader[3] = FrameType.HEADERS.getIdByte();
// Flags. End of stream
frameHeader[4] = 0x01;
} else {
frameHeader[3] = FrameType.CONTINUATION.getIdByte();
}
if (left == thisTime) {
// Flags. End of headers
frameHeader[4] = (byte) (frameHeader[4] + 0x04);
}
// Stream id
ByteUtil.set31Bits(frameHeader, 5, streamId);
}
@Test
public void testCookieLimit1() throws Exception {
doTestCookieLimit(1, 0);
}
@Test
public void testCookieLimit2() throws Exception {
doTestCookieLimit(2, 0);
}
@Test
public void testCookieLimit100() throws Exception {
doTestCookieLimit(100, 0);
}
@Test
public void testCookieLimit100WithLimit50() throws Exception {
doTestCookieLimit(100, 50, 1);
}
@Test
public void testCookieLimit200() throws Exception {
doTestCookieLimit(200, 0);
}
@Test
public void testCookieLimit201() throws Exception {
doTestCookieLimit(201, 1);
}
private void doTestCookieLimit(int cookieCount, int failMode) throws Exception {
doTestCookieLimit(cookieCount, Constants.DEFAULT_MAX_COOKIE_COUNT, failMode);
}
private void doTestCookieLimit(int cookieCount, int maxCookieCount, int failMode) throws Exception {
enableHttp2();
Connector connector = getTomcatInstance().getConnector();
connector.setMaxCookieCount(maxCookieCount);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
output.setTraceBody(true);
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(8192);
List<String[]> customHeaders = new ArrayList<>();
for (int i = 0; i < cookieCount; i++) {
customHeaders.add(new String[] { "Cookie", "a" + cookieCount + "=b" + cookieCount });
}
populateHeadersPayload(headersPayload, customHeaders, "/cookie");
populateFrameHeader(frameHeader, 0, headersPayload.limit(), headersPayload.limit(), 3);
writeFrame(frameHeader, headersPayload);
switch (failMode) {
case 0: {
parser.readFrame();
parser.readFrame();
parser.readFrame();
System.out.println(output.getTrace());
Assert.assertEquals(getCookieResponseTrace(3, cookieCount), output.getTrace());
break;
}
case 1: {
// Check status is 400
parser.readFrame();
Assert.assertTrue(output.getTrace(),
output.getTrace().startsWith("3-HeadersStart\n3-Header-[:status]-[400]"));
output.clearTrace();
// Check EOS followed by error page body
parser.readFrame();
Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("3-EndOfStream\n3-Body-<!doctype"));
break;
}
default: {
Assert.fail("Unknown failure mode specified");
}
}
}
@Test
public void testPostWithTrailerHeadersDefaultLimit() throws Exception {
doTestPostWithTrailerHeaders(Constants.DEFAULT_MAX_TRAILER_COUNT, Constants.DEFAULT_MAX_TRAILER_SIZE,
FailureMode.NONE);
}
@Test
public void testPostWithTrailerHeadersCount0() throws Exception {
doTestPostWithTrailerHeaders(0, Constants.DEFAULT_MAX_TRAILER_SIZE, FailureMode.STREAM_RESET);
}
@Test
public void testPostWithTrailerHeadersSize0() throws Exception {
doTestPostWithTrailerHeaders(Constants.DEFAULT_MAX_TRAILER_COUNT, 0, FailureMode.CONNECTION_RESET);
}
private void doTestPostWithTrailerHeaders(int maxTrailerCount, int maxTrailerSize, FailureMode failMode)
throws Exception {
enableHttp2();
((AbstractHttp11Protocol<?>) http2Protocol.getHttp11Protocol()).setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
http2Protocol.setMaxTrailerCount(maxTrailerCount);
((AbstractHttp11Protocol<?>) http2Protocol.getHttp11Protocol()).setMaxTrailerSize(maxTrailerSize);
// Disable overhead protection for window update as it breaks some tests
http2Protocol.setOverheadWindowUpdateThreshold(0);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(256);
byte[] trailerFrameHeader = new byte[9];
ByteBuffer trailerPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload, null, true, 3);
buildTrailerHeaders(trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
writeFrame(dataFrameHeader, dataPayload);
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
switch (failMode) {
case NONE: {
parser.readFrame();
parser.readFrame();
parser.readFrame();
parser.readFrame();
String len = Integer.toString(256 + TRAILER_HEADER_VALUE.length());
Assert.assertEquals("0-WindowSize-[256]\n" + "3-WindowSize-[256]\n" + "3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" + "3-Header-[content-length]-[" + len + "]\n" +
"3-Header-[date]-[" + DEFAULT_DATE + "]\n" + "3-HeadersEnd\n" + "3-Body-" + len + "\n" +
"3-EndOfStream\n", output.getTrace());
break;
}
case STREAM_RESET: {
// NIO2 can sometimes send window updates depending timing
skipWindowSizeFrames();
// Async I/O can sometimes result in a stream closed reset before
// the enhance your calm reset
if ("3-RST-[5]\n".equals(output.getTrace())) {
output.clearTrace();
parser.readFrame();
}
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
break;
}
case STREAM_RESET_THEN_CONNECTION_RESET: {
Assert.fail("Not used");
break;
}
case CONNECTION_RESET: {
// NIO2 can sometimes send window updates depending timing
skipWindowSizeFrames();
// This message uses i18n and needs to be used in a regular
// expression (since we don't know the connection ID). Generate the
// string as a regular expression and then replace '[' and ']' with
// the escaped values.
String limitMessage = sm.getString("http2Parser.headerLimitSize", "\\p{XDigit}++", "3");
limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]");
MatcherAssert.assertThat(output.getTrace(),
RegexMatcher.matchesRegex("0-Goaway-\\[3\\]-\\[11\\]-\\[" + limitMessage + "\\]"));
break;
}
}
}
private enum FailureMode {
NONE,
STREAM_RESET,
CONNECTION_RESET,
STREAM_RESET_THEN_CONNECTION_RESET,
}
private static class RegexMatcher extends TypeSafeMatcher<String> {
private final String pattern;
RegexMatcher(String pattern) {
this.pattern = pattern;
}
@Override
public void describeTo(Description description) {
description.appendText("match to regular expression pattern [" + pattern + "]");
}
@Override
protected boolean matchesSafely(String item) {
return item.matches(pattern);
}
public static RegexMatcher matchesRegex(String pattern) {
return new RegexMatcher(pattern);
}
}
}