This commit is contained in:
2024-11-30 19:03:49 +08:00
commit 1e6763c160
3806 changed files with 737676 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
/*
* 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.io.InputStream;
import java.nio.ByteBuffer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
public class TestAbortedUpload extends Http2TestBase {
@Test
public void testAbortedRequest() throws Exception {
http2Connect();
http2Protocol.setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
int bodySize = 8192;
int bodyCount = 20;
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(bodySize);
byte[] trailerFrameHeader = new byte[9];
ByteBuffer trailerPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload,
null, trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
for (int i = 0; i < bodyCount; i++) {
writeFrame(dataFrameHeader, dataPayload);
}
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
// The actual response depends on timing issues. Particularly how much
// data is transferred in StreamInputBuffer inBuffer to outBuffer on the
// first read.
while (output.getTrace().length() == 0) {
try {
parser.readFrame(true);
if ("3-RST-[3]\n".equals(output.getTrace())) {
output.clearTrace();
}
} catch (IOException ioe) {
// Might not be any further frames after the reset
break;
}
}
if (output.getTrace().startsWith("0-WindowSize-[")) {
String trace = output.getTrace();
int size = Integer.parseInt(trace.substring(14, trace.length() - 2));
output.clearTrace();
// Window updates always come in pairs
parser.readFrame(true);
Assert.assertEquals("3-WindowSize-[" + size + "]\n", output.getTrace());
}
}
@Override
protected void configureAndStartWebApplication() throws LifecycleException {
Tomcat tomcat = getTomcatInstance();
// Retain '/simple' url-pattern since it enables code re-use
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "abort", new AbortServlet());
ctxt.addServletMappingDecoded("/simple", "abort");
tomcat.start();
}
private static class AbortServlet extends SimpleServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Read upto 128 bytes and then return a 403 response
InputStream is = req.getInputStream();
byte[] buf = new byte[128];
int toRead = 128;
int read = is.read(buf);
while (read != -1 && toRead > 0) {
toRead -= read;
read = is.read(buf);
}
if (toRead == 0) {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
} else {
resp.setStatus(HttpServletResponse.SC_OK);
}
}
}
}

View File

@@ -0,0 +1,286 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/*
* This tests use A=1, B=2, etc to map stream IDs to the names used in the
* figures.
*/
public class TestAbstractStream {
@Test
public void testDependenciesFig3() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
// Action
d.rePrioritise(a, false, 16);
// Check parents
Assert.assertEquals(handler, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(a, d.getParentStream());
// Check children
Assert.assertEquals(3, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertTrue(a.getChildStreams().contains(d));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, c.getChildStreams().size());
Assert.assertEquals(0, d.getChildStreams().size());
}
@Test
public void testDependenciesFig4() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
// Action
d.rePrioritise(a, true, 16);
// Check parents
Assert.assertEquals(handler, a.getParentStream());
Assert.assertEquals(d, b.getParentStream());
Assert.assertEquals(d, c.getParentStream());
Assert.assertEquals(a, d.getParentStream());
// Check children
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(d));
Assert.assertEquals(2, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(b));
Assert.assertTrue(d.getChildStreams().contains(c));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, c.getChildStreams().size());
}
@Test
public void testDependenciesFig5NonExclusive() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(c, false, 16);
e.rePrioritise(c, false, 16);
f.rePrioritise(d, false, 16);
// Action
a.rePrioritise(d, false, 16);
// Check parents
Assert.assertEquals(handler, d.getParentStream());
Assert.assertEquals(d, f.getParentStream());
Assert.assertEquals(d, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(c, e.getParentStream());
// Check children
Assert.assertEquals(2, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(a));
Assert.assertTrue(d.getChildStreams().contains(f));
Assert.assertEquals(0, f.getChildStreams().size());
Assert.assertEquals(2, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(e));
Assert.assertEquals(0, e.getChildStreams().size());
}
@Test
public void testDependenciesFig5Exclusive() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(c, false, 16);
e.rePrioritise(c, false, 16);
f.rePrioritise(d, false, 16);
// Action
a.rePrioritise(d, true, 16);
// Check parents
Assert.assertEquals(handler, d.getParentStream());
Assert.assertEquals(d, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(a, f.getParentStream());
Assert.assertEquals(c, e.getParentStream());
// Check children
Assert.assertEquals(1, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(a));
Assert.assertEquals(3, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertTrue(a.getChildStreams().contains(f));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, f.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(e));
Assert.assertEquals(0, e.getChildStreams().size());
}
@Test
public void testCircular01() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(b, false, 16);
// Action
a.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(c, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(a));
}
@Test
public void testCircular02() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(b, false, 16);
e.rePrioritise(d, false, 16);
f.rePrioritise(e, false, 16);
// Action
a.rePrioritise(f, false, 16);
d.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(f, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
Assert.assertEquals(c, d.getParentStream());
Assert.assertEquals(d, e.getParentStream());
Assert.assertEquals(e, f.getParentStream());
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(d));
Assert.assertEquals(1, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(e));
Assert.assertEquals(1, e.getChildStreams().size());
Assert.assertTrue(e.getChildStreams().contains(f));
Assert.assertEquals(1, f.getChildStreams().size());
Assert.assertTrue(f.getChildStreams().contains(a));
}
// https://bz.apache.org/bugzilla/show_bug.cgi?id=61682
@Test
public void testCircular03() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(3), handler);
Stream c = new Stream(Integer.valueOf(5), handler);
Stream d = new Stream(Integer.valueOf(7), handler);
// Action
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(b, false, 16);
c.rePrioritise(handler, false, 16);
a.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(c, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
Assert.assertEquals(b, d.getParentStream());
// This triggers the StackOverflowError
Assert.assertTrue(c.isDescendant(d));
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(a));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(1, b.getChildStreams().size());
Assert.assertTrue(b.getChildStreams().contains(d));
Assert.assertEquals(0, d.getChildStreams().size());
}
}

View File

@@ -0,0 +1,277 @@
/*
* 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.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
/*
* Based on
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62614
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62620
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62628
*/
@RunWith(Parameterized.class)
public class TestAsync extends Http2TestBase {
private static final int BLOCK_SIZE = 0x8000;
@Parameterized.Parameters(name = "{index}: expandConnectionFirst[{0}], " +
"connectionUnlimited[{1}], streamUnlimited[{2}], useNonContainerThreadForWrite[{3}]," +
"largeInitialWindow[{4}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
for (Boolean expandConnectionFirst : booleans) {
for (Boolean connectionUnlimited : booleans) {
for (Boolean streamUnlimited : booleans) {
for (Boolean useNonContainerThreadForWrite : booleans) {
for (Boolean largeInitialWindow : booleans) {
parameterSets.add(new Object[] {
expandConnectionFirst, connectionUnlimited, streamUnlimited,
useNonContainerThreadForWrite, largeInitialWindow
});
}
}
}
}
}
return parameterSets;
}
private final boolean expandConnectionFirst;
private final boolean connectionUnlimited;
private final boolean streamUnlimited;
private final boolean useNonContainerThreadForWrite;
private final boolean largeInitialWindow;
public TestAsync(boolean expandConnectionFirst, boolean connectionUnlimited,
boolean streamUnlimited, boolean useNonContainerThreadForWrite,
boolean largeInitialWindow) {
this.expandConnectionFirst = expandConnectionFirst;
this.connectionUnlimited = connectionUnlimited;
this.streamUnlimited = streamUnlimited;
this.useNonContainerThreadForWrite = useNonContainerThreadForWrite;
this.largeInitialWindow = largeInitialWindow;
}
@Test
public void testEmptyWindow() throws Exception {
int blockCount = 8;
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async",
new AsyncServlet(blockCount, useNonContainerThreadForWrite));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
int startingWindowSize;
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after initial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
if (largeInitialWindow) {
startingWindowSize = ((1 << 17) - 1);
SettingValue sv =
new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), startingWindowSize);
sendSettings(0, false, sv);
// Test code assumes connection window and stream window size are the same at the start
sendWindowUpdate(0, startingWindowSize - ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
} else {
startingWindowSize = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
}
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
if (connectionUnlimited) {
// Effectively unlimited for this test
sendWindowUpdate(0, blockCount * BLOCK_SIZE * 2);
}
if (streamUnlimited) {
// Effectively unlimited for this test
sendWindowUpdate(3, blockCount * BLOCK_SIZE * 2);
}
// Headers
parser.readFrame(true);
// Body
if (!connectionUnlimited || !streamUnlimited) {
while (output.getBytesRead() < startingWindowSize) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(startingWindowSize, output.getBytesRead());
// Increase the Window size (50% of total body)
int windowSizeIncrease = blockCount * BLOCK_SIZE / 2;
if (expandConnectionFirst) {
sendWindowUpdate(0, windowSizeIncrease);
sendWindowUpdate(3, windowSizeIncrease);
} else {
sendWindowUpdate(3, windowSizeIncrease);
sendWindowUpdate(0, windowSizeIncrease);
}
while (output.getBytesRead() < startingWindowSize + windowSizeIncrease) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(startingWindowSize + windowSizeIncrease, output.getBytesRead());
// Increase the Window size
if (expandConnectionFirst) {
sendWindowUpdate(0, windowSizeIncrease);
sendWindowUpdate(3, windowSizeIncrease);
} else {
sendWindowUpdate(3, windowSizeIncrease);
sendWindowUpdate(0, windowSizeIncrease);
}
}
while (!output.getTrace().endsWith("3-EndOfStream\n")) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals((long) blockCount * BLOCK_SIZE, output.getBytesRead());
}
public static class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int blockLimit;
private final boolean useNonContainerThreadForWrite;
private final transient ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private transient volatile Future<?> future;
public AsyncServlet(int blockLimit, boolean useNonContainerThreadForWrite) {
this.blockLimit = blockLimit;
this.useNonContainerThreadForWrite = useNonContainerThreadForWrite;
}
/*
* Not thread-safe. OK for this test. NOt OK for use in the real world.
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/binary");
final ServletOutputStream output = response.getOutputStream();
output.setWriteListener(new WriteListener() {
// Intermittent CI errors were observed where the response body
// was exactly one block too small. Use an AtomicInteger to be
// sure blockCount is thread-safe.
final AtomicInteger blockCount = new AtomicInteger(0);
byte[] bytes = new byte[BLOCK_SIZE];
@Override
public void onWritePossible() throws IOException {
if (useNonContainerThreadForWrite) {
future = scheduler.schedule(new Runnable() {
@Override
public void run() {
try {
write();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}, 200, TimeUnit.MILLISECONDS);
} else {
write();
}
}
private void write() throws IOException {
while (output.isReady()) {
blockCount.incrementAndGet();
output.write(bytes);
if (blockCount.get() == blockLimit) {
asyncContext.complete();
scheduler.shutdown();
return;
}
}
}
@Override
public void onError(Throwable t) {
if (future != null) {
future.cancel(false);
}
t.printStackTrace();
}
});
}
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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 javax.servlet.AsyncContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
/*
* Based on
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62635
*
* Note: Calling blocking I/O methods (such as flushBuffer()) during
* non-blocking I/O is explicitly called out as illegal in the Servlet
* specification but also goes on to say the behaviour if such a call is
* made is undefined. Which means it is OK if the call works as expected
* (a non-blocking flush is triggered) :).
* If any of these tests fail, that should not block a release since -
* while the specification allows this to work - it doesn't require that
* it does work.
*/
public class TestAsyncFlush extends Http2TestBase {
private static final int BLOCK_SIZE = 1024;
@Test
public void testFlush() throws Exception {
int blockCount = 2048;
int targetSize = BLOCK_SIZE * blockCount;
int totalWindow = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncFlushServlet(blockCount));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after intial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
// Send request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
// Body
while (output.getBytesRead() < targetSize ) {
if (output.getBytesRead() == totalWindow) {
sendWindowUpdate(3, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
sendWindowUpdate(0, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
totalWindow += ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
}
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(targetSize, output.getBytesRead());
}
public static class AsyncFlushServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int blockLimit;
public AsyncFlushServlet(int blockLimit) {
this.blockLimit = blockLimit;
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws IOException {
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/binary");
final ServletOutputStream output = response.getOutputStream();
output.setWriteListener(new WriteListener() {
int blockCount;
byte[] bytes = new byte[BLOCK_SIZE];
@Override
public void onWritePossible() throws IOException {
while (output.isReady()) {
blockCount++;
output.write(bytes);
if (blockCount % 5 == 0) {
response.flushBuffer();
}
if (blockCount == blockLimit) {
asyncContext.complete();
return;
}
}
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
});
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* 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.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
public class TestAsyncTimeout extends Http2TestBase {
@Test
public void testTimeout() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
// This is the target of the HTTP/2 upgrade request
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
// This is the servlet that does that actual test
// This latch is used to signal that that async thread used by the test
// has ended. It isn;t essential to the test but it allows the test to
// complete without Tmcat logging an error about a still running thread.
CountDownLatch latch = new CountDownLatch(1);
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncTimeoutServlet(latch));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after initial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
// Include the response body in the trace so we can check for the PASS /
// FAIL text.
output.setTraceBody(true);
// Send request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
// Body
parser.readFrame(true);
// Check that the expected text was received
String trace = output.getTrace();
Assert.assertTrue(trace, trace.contains("PASS"));
latch.await(10, TimeUnit.SECONDS);
}
public static class AsyncTimeoutServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final CountDownLatch latch;
public AsyncTimeoutServlet(CountDownLatch latch) {
this.latch = latch;
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// The idea of this test is that the timeout kicks in after 2
// seconds and stops the async thread early rather than letting it
// complete the full 5 seconds of processing.
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
// Only want to call complete() once (else we get stack traces in
// the logs so use this to track when complete() is called).
AtomicBoolean completeCalled = new AtomicBoolean(false);
Ticker ticker = new Ticker(asyncContext, completeCalled);
TimeoutListener listener = new TimeoutListener(latch, ticker, completeCalled);
asyncContext.addListener(listener);
asyncContext.setTimeout(2000);
ticker.start();
}
}
private static class Ticker extends Thread {
private final AsyncContext asyncContext;
private final AtomicBoolean completeCalled;
private volatile boolean running = true;
public Ticker(AsyncContext asyncContext, AtomicBoolean completeCalled) {
this.asyncContext = asyncContext;
this.completeCalled = completeCalled;
}
public void end() {
running = false;
}
@Override
public void run() {
try {
PrintWriter pw = asyncContext.getResponse().getWriter();
int counter = 0;
// If the test works running will be set too false before
// counter reaches 50.
while (running && counter < 50) {
Thread.sleep(100);
counter++;
pw.print("Tick " + counter);
}
// Need to call complete() here if the test fails but complete()
// should have been called by the listener. Use the flag to make
// sure we only call complete once.
if (completeCalled.compareAndSet(false, true)) {
asyncContext.complete();
}
} catch (IOException | InterruptedException e) {
// Ignore
}
}
}
private static class TimeoutListener implements AsyncListener {
private final AtomicBoolean ended = new AtomicBoolean(false);
private final CountDownLatch latch;
private final Ticker ticker;
private final AtomicBoolean completeCalled;
public TimeoutListener(CountDownLatch latch, Ticker ticker, AtomicBoolean completeCalled) {
this.latch = latch;
this.ticker = ticker;
this.completeCalled = completeCalled;
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
ticker.end();
if (ended.compareAndSet(false, true)) {
PrintWriter pw = event.getAsyncContext().getResponse().getWriter();
pw.write("PASS");
pw.flush();
// If the timeout fires we should always need to call complete()
// here but use the flag to be safe.
if (completeCalled.compareAndSet(false, true)) {
event.getAsyncContext().complete();
}
}
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onError(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
if (ended.compareAndSet(false, true)) {
PrintWriter pw = event.getAsyncContext().getResponse().getWriter();
pw.write("FAIL");
pw.flush();
}
try {
// Wait for the async thread to end before we signal that the
// test is complete. This avoids logging an exception about a
// still running thread when the unit test shuts down.
ticker.join();
latch.countDown();
} catch (InterruptedException e) {
// Ignore
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
public class TestByteUtil {
@Test
public void testGet31Bits() {
byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
int result = ByteUtil.get31Bits(input, 0);
Assert.assertEquals(0x7fffffff, result);
}
@Test
public void testGetFourBytes() {
byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
long result = ByteUtil.getFourBytes(input, 0);
Assert.assertEquals(0xffffffffL, result);
}
}

View File

@@ -0,0 +1,146 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
import org.apache.tomcat.util.http.MimeHeaders;
public class TestHpack {
@Test
public void testEncode() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("header1").setString("value1");
headers.setValue(":status").setString("200");
headers.setValue("header2").setString("value2");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
// Size is supposed to be 33 without huffman, or 27 with it
// TODO: use the HpackHeaderFunction to enable huffman predictably
Assert.assertEquals(27, output.remaining());
output.clear();
encoder.encode(headers, output);
output.flip();
// Size is now 3 after using the table
Assert.assertEquals(3, output.remaining());
}
@Test
public void testDecode() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("header1").setString("value1");
headers.setValue(":status").setString("200");
headers.setValue("header2").setString("value2");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
// Redo (table is supposed to be updated)
output.clear();
encoder.encode(headers, output);
output.flip();
headers2.recycle();
Assert.assertEquals(3, output.remaining());
// Check that the decoder is using the table right
decoder.decode(output);
Assert.assertEquals("value2", headers2.getHeader("header2"));
}
private static class HeadersListener implements HpackDecoder.HeaderEmitter {
private final MimeHeaders headers;
public HeadersListener(MimeHeaders headers) {
this.headers = headers;
}
@Override
public void emitHeader(String name, String value) {
headers.setValue(name).setString(value);
}
@Override
public void setHeaderException(StreamException streamException) {
// NO-OP
}
@Override
public void validateHeaders() throws StreamException {
// NO-OP
}
}
@Test
public void testHeaderValueBug60451() throws HpackException {
doTestHeaderValueBug60451("fooébar");
}
@Test
public void testHeaderValueFullRange() {
for (int i = 0; i < 256; i++) {
// Skip the control characters except VTAB
if (i == 9 || i > 31 && i < 127 || i > 127) {
try {
doTestHeaderValueBug60451("foo" + Character.toString((char) i) + "bar");
} catch (Exception e) {
e.printStackTrace();
Assert.fail(e.getMessage() + "[" + i + "]");
}
}
}
}
@Test(expected=HpackException.class)
public void testExcessiveStringLiteralPadding() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("X-test").setString("foobar");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
// Hack the output buffer to extend the EOS marker for the header value
// by another byte
output.array()[7] = (byte) -122;
output.put((byte) -1);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
}
private void doTestHeaderValueBug60451(String filename) throws HpackException {
String headerName = "Content-Disposition";
String headerValue = "attachment;filename=\"" + filename + "\"";
MimeHeaders headers = new MimeHeaders();
headers.setValue(headerName).setString(headerValue);
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
Assert.assertEquals(headerValue, headers2.getHeader(headerName));
}
}

View File

@@ -0,0 +1,174 @@
/*
* 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.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.tomcat.util.res.StringManager;
public class TestHttp2InitialConnection extends Http2TestBase {
private TestData testData;
@Test
public void testValidHostHeader() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
hostHeaders.add("localhost:8080");
testData = new TestData(hostHeaders, 200);
http2Connect();
}
@Test
public void testMultipleHostHeaders() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
hostHeaders.add("localhost:8080");
hostHeaders.add("localhost:8081");
testData = new TestData(hostHeaders, 400);
http2Connect();
}
@Test
public void testNoHostHeader() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
testData = new TestData(hostHeaders, 400);
http2Connect();
}
@Override
protected void doHttpUpgrade(String connection, String upgrade, String settings,
boolean validate) throws IOException {
StringBuilder request = new StringBuilder();
request.append("GET /simple HTTP/1.1\r\n");
for (String hostHeader : testData.getHostHeaders()) {
request.append("Host: ");
request.append(hostHeader);
request.append("\r\n");
}
// Connection
request.append("Connection: ");
request.append(connection);
request.append("\r\n");
// Upgrade
request.append("Upgrade: ");
request.append(upgrade);
request.append("\r\n");
// Settings
request.append(settings);
// Locale - Force the en Locale else the i18n on the error page changes
// the size of the response body and that triggers a failure as the test
// checks the exact response length
request.append("Accept-Language: en\r\n");
// Request terminator
request.append("\r\n");
byte[] upgradeRequest = request.toString().getBytes(StandardCharsets.ISO_8859_1);
os.write(upgradeRequest);
os.flush();
if (validate) {
Assert.assertTrue("Failed to read HTTP Upgrade response",
readHttpUpgradeResponse());
}
}
@Override
protected String getResponseBodyFrameTrace(int streamId, String body) {
if (testData.getExpectedStatus() == 200) {
return super.getResponseBodyFrameTrace(streamId, body);
} else if (testData.getExpectedStatus() == 400) {
/*
* Need to be careful here. The test wants the exact content length
* in bytes.
* This will vary depending on where the test is run due to:
* - The length of the version string that appears once in the error
* page
* - The status header uses a UTF-8 EN dash. When running in an IDE
* the UTF-8 properties files will be used directly rather than
* after native2ascii conversion.
*
* Note: The status header appears twice in the error page.
*/
int serverInfoLength = ServerInfo.getServerInfo().getBytes().length;
StringManager sm = StringManager.getManager(
ErrorReportValve.class.getPackage().getName(), Locale.ENGLISH);
String reason = sm.getString("http." + testData.getExpectedStatus() + ".reason");
int descriptionLength = sm.getString("http." + testData.getExpectedStatus() + ".desc")
.getBytes(StandardCharsets.UTF_8).length;
int statusHeaderLength = sm
.getString("errorReportValve.statusHeader",
String.valueOf(testData.getExpectedStatus()), reason)
.getBytes(StandardCharsets.UTF_8).length;
int typeLabelLength = sm.getString("errorReportValve.type")
.getBytes(StandardCharsets.UTF_8).length;
int statusReportLabelLength = sm.getString("errorReportValve.statusReport")
.getBytes(StandardCharsets.UTF_8).length;
int descriptionLabelLength = sm.getString("errorReportValve.description")
.getBytes(StandardCharsets.UTF_8).length;
// 196 bytes is the static length of the pure HTML code from the ErrorReportValve
int len = 196 + org.apache.catalina.util.TomcatCSS.TOMCAT_CSS
.getBytes(StandardCharsets.UTF_8).length +
typeLabelLength + statusReportLabelLength + descriptionLabelLength +
descriptionLength + serverInfoLength + statusHeaderLength * 2;
String contentLength = String.valueOf(len);
return getResponseBodyFrameTrace(streamId,
testData.getExpectedStatus(), "text/html;charset=utf-8",
"en", contentLength, contentLength);
} else {
Assert.fail();
// To keep the IDE happy
return null;
}
}
private static class TestData {
private final List<String> hostHeaders;
private final int expectedStatus;
public TestData(List<String> hostHeaders, int expectedStatus) {
this.hostHeaders = hostHeaders;
this.expectedStatus = expectedStatus;
}
public List<String> getHostHeaders() {
return hostHeaders;
}
public int getExpectedStatus() {
return expectedStatus;
}
}
}

View File

@@ -0,0 +1,550 @@
/*
* 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.TypeSafeMatcher;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http2.HpackEncoder.State;
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 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(true);
parser.readFrame(true);
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.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);
http2Protocol.setMaxHeaderSize(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(true);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
Assert.assertNull(e);
break;
}
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", "\\d++", "3");
limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]");
// Connection reset. Connection ID will vary so use a pattern
// On some platform / Connector combinations (e.g. Windows / APR),
// 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(true);
Assert.assertThat(output.getTrace(), RegexMatcher.matchesRegex(
"0-Goaway-\\[1\\]-\\[11\\]-\\[" + limitMessage + "\\]"));
} catch (IOException se) {
// 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("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(true);
parser.readFrame(true);
parser.readFrame(true);
System.out.println(output.getTrace());
Assert.assertEquals(getCookieResponseTrace(3, cookieCount), output.getTrace());
break;
}
case 1: {
// Check status is 400
parser.readFrame(true);
Assert.assertTrue(output.getTrace(), output.getTrace().startsWith(
"3-HeadersStart\n3-Header-[:status]-[400]"));
output.clearTrace();
// Check EOS followed by error page body
parser.readFrame(true);
Assert.assertTrue(output.getTrace(), output.getTrace().startsWith("3-EndOfStream\n3-Body-<!doctype"));
break;
}
default: {
Assert.fail("Unknown failure mode specified");
}
}
}
@Test
public void doTestPostWithTrailerHeadersDefaultLimit() throws Exception{
doTestPostWithTrailerHeaders(Constants.DEFAULT_MAX_TRAILER_COUNT,
Constants.DEFAULT_MAX_TRAILER_SIZE, FailureMode.NONE);
}
@Test
public void doTestPostWithTrailerHeadersCount0() throws Exception{
doTestPostWithTrailerHeaders(0, Constants.DEFAULT_MAX_TRAILER_SIZE,
FailureMode.STREAM_RESET);
}
@Test
public void doTestPostWithTrailerHeadersSize0() throws Exception{
doTestPostWithTrailerHeaders(Constants.DEFAULT_MAX_TRAILER_COUNT, 0,
FailureMode.CONNECTION_RESET);
}
private void doTestPostWithTrailerHeaders(int maxTrailerCount, int maxTrailerSize,
FailureMode failMode) throws Exception {
enableHttp2();
http2Protocol.setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
http2Protocol.setMaxTrailerCount(maxTrailerCount);
http2Protocol.setMaxTrailerSize(maxTrailerSize);
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, trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
writeFrame(dataFrameHeader, dataPayload);
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
switch (failMode) {
case NONE: {
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
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();
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
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", "\\d++", "3");
limitMessage = limitMessage.replace("[", "\\[").replace("]", "\\]");
Assert.assertThat(output.getTrace(), RegexMatcher.matchesRegex(
"0-Goaway-\\[3\\]-\\[11\\]-\\[" + limitMessage + "\\]"));
break;
}
}
}
private enum FailureMode {
NONE,
STREAM_RESET,
CONNECTION_RESET,
}
private static class RegexMatcher extends TypeSafeMatcher<String> {
private final String pattern;
public 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);
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* 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.charset.StandardCharsets;
import org.junit.Test;
/**
* Unit tests for Section 3.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_3_2 extends Http2TestBase {
// Note: Tests for zero/multiple HTTP2-Settings fields can be found below
// in the tests for section 3.2.1
// TODO: Test initial requests with bodies of various sizes
@Test
public void testConnectionNoHttp2Support() throws Exception {
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testConnectionUpgradeWrongProtocol() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2", EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test(timeout=10000)
public void testConnectionNoPreface() throws Exception {
setupAsFarAsUpgrade();
// If we don't send the preface the server should kill the connection.
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test(timeout=10000)
public void testConnectionIncompletePrefaceStart() throws Exception {
setupAsFarAsUpgrade();
// If we send an incomplete preface the server should kill the
// connection.
os.write("PRI * HTTP/2.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
os.flush();
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test(timeout=10000)
public void testConnectionInvalidPrefaceStart() throws Exception {
setupAsFarAsUpgrade();
// If we send an incomplete preface the server should kill the
// connection.
os.write("xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxxx".getBytes(
StandardCharsets.ISO_8859_1));
os.flush();
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test
public void testConnectionUpgradeFirstResponse() throws Exception{
super.http2Connect();
}
private void setupAsFarAsUpgrade() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
}
//------------------------------------------------------------ Section 3.2.1
@Test
public void testZeroHttp2Settings() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(Http2TestBase.DEFAULT_CONNECTION_HEADER_VALUE, "h2c", "", false);
parseHttp11Response();
}
@Test
public void testMultipleHttp2Settings() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(Http2TestBase.DEFAULT_CONNECTION_HEADER_VALUE, "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER +
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testMissingConnectionValue() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("Upgrade", "h2c", Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testSplitConnectionValue01() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("Upgrade\r\nConnection: HTTP2-Settings", "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, true);
sendClientPreface();
validateHttp2InitialResponse();
}
@Test
public void testSplitConnectionValue02() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("HTTP2-Settings\r\nConnection: Upgrade", "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, true);
sendClientPreface();
validateHttp2InitialResponse();
}
// No need to test how trailing '=' are handled here. HTTP2Settings payloads
// are always a multiple of 6 long which means valid payloads never end in
// '='. Invalid payloads will be rejected anyway.
}

View File

@@ -0,0 +1,39 @@
/*
* 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 org.junit.Test;
public class TestHttp2Section_3_5 extends Http2TestBase {
@Test(expected=IOException.class)
public void testNoConnectionPreface() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
// Should send client preface here
sendPing();
// Send several pings else server will block waiting for the client
// preface which is longer than a single ping.
sendPing();
sendPing();
validateHttp2InitialResponse();
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_1 extends Http2TestBase {
private static final byte[] UNKNOWN_FRAME = new byte[] {
0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00 };
// TODO: Tests for over-sized frames. Better located in tests for section 6?
@Test
public void testUnknownFrameType() throws Exception {
http2Connect();
os.write(UNKNOWN_FRAME);
os.flush();
sendSimpleGetRequest(3);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
// TODO: Tests for unexpected flags. Better located in tests for section 6?
@Test
public void testReservedBitIgnored() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Tweak the header to set the reserved bit
frameHeader[5] = (byte) (frameHeader[5] | 0x80);
// Process the request
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_2 extends Http2TestBase {
@Test
public void testFrameSizeLimitsTooBig() throws Exception {
// HTTP2 upgrade
http2Connect();
// Overly large settings
// Settings have to be a multiple of six
int settingsCount = (ConnectionSettingsBase.DEFAULT_MAX_FRAME_SIZE / 6) + 1;
int size = settingsCount * 6;
byte[] settings = new byte[size + 9];
// Header
// Length
ByteUtil.setThreeBytes(settings, 0, size);
// Type
settings[3] = FrameType.SETTINGS.getIdByte();
// No flags
// Stream 0
// payload
for (int i = 0; i < settingsCount; i++) {
// Enable server push over and over again
ByteUtil.setTwoBytes(settings, (i * 6) + 9, 2);
ByteUtil.setFourBytes(settings, (i * 6) + 9 + 2, 1);
}
os.write(settings);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsTooBig() throws Exception {
// HTTP2 upgrade
http2Connect();
// Overly large ping
byte[] ping = new byte[109];
// Header
// Length
ByteUtil.setThreeBytes(ping, 0, 100);
// Type
ping[3] = FrameType.PING.getIdByte();
// No flags
// Stream 0
// Empty payload
os.write(ping);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsTooSmall() throws Exception {
// HTTP2 upgrade
http2Connect();
// Too small ping
byte[] ping = new byte[9];
// Header
// Length 0
// Type
ping[3] = FrameType.PING.getIdByte();
// No flags
// Stream 0
// Empty payload
os.write(ping);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsStream() throws Exception {
// HTTP2 upgrade
http2Connect();
// Invalid priority
byte[] priority = new byte[9];
// Header
// Length 0
// Type
priority[3] = FrameType.PRIORITY.getIdByte();
// No flags
// Stream 3
ByteUtil.set31Bits(priority, 5, 3);
// Empty payload
os.write(priority);
// Read Stream reset frame
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().startsWith("3-RST-[6]"));
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.3 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_3 extends Http2TestBase {
@Test
public void testHeaderDecodingError() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Try and corrupt the headerPayload
headersPayload.put(0, (byte) (headersPayload.get(0) + 128));
// Process the request
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
@Test
public void testHeaderContinuationContiguous() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
// Part 2
headersPayload.clear();
buildSimpleGetRequestPart2(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
// headers, body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testHeaderContinuationNonContiguous() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
sendPing();
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
}

View File

@@ -0,0 +1,288 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 5.§ of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_1 extends Http2TestBase {
@Test
public void testIdleStateInvalidFrame01() throws Exception {
http2Connect();
sendWindowUpdate(3, 200);
handleGoAwayResponse(1);
}
@Test
public void testIdleStateInvalidFrame02() throws Exception {
http2Connect();
sendData(3, new byte[] {});
handleGoAwayResponse(1);
}
// TODO: reserved local
// TODO: reserved remote
@Test
public void halfClosedRemoteInvalidFrame() throws Exception {
http2Connect();
// This half-closes the stream since it includes the end of stream flag
sendSimpleGetRequest(3);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
output.clearTrace();
// This should trigger a connection error
sendData(3, new byte[] {});
handleGoAwayResponse(3, Http2Error.STREAM_CLOSED);
}
@Test
public void testClosedInvalidFrame01() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Remove the end of stream and end of headers flags
frameHeader[4] = 0;
// Process the request
writeFrame(frameHeader, headersPayload);
// Send a rst
sendRst(3, Http2Error.INTERNAL_ERROR.getCode());
// Then try sending some data (which should fail)
sendData(3, new byte[] {});
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().startsWith("3-RST-[" + Http2Error.STREAM_CLOSED.getCode() + "]"));
}
@Test
public void testClosedInvalidFrame02() throws Exception {
http2Connect();
// Stream 1 is closed. This should trigger a connection error
sendData(1, new byte[] {});
handleGoAwayResponse(1, Http2Error.STREAM_CLOSED);
}
// TODO: Invalid frames for each of the remaining states
// Section 5.1.1
@Test
public void testClientSendEvenStream() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 4);
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1);
}
@Test
public void testClientSendOldStream() throws Exception {
http2Connect();
sendSimpleGetRequest(5);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace());
output.clearTrace();
// Build the simple request on an old stream
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
os.write(frameHeader);
os.flush();
handleGoAwayResponse(5);
}
@Test
public void testImplicitClose() throws Exception {
http2Connect();
sendPriority(3, 0, 16);
sendPriority(5, 0, 16);
sendSimpleGetRequest(5);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace());
output.clearTrace();
// Should trigger an error since stream 3 should have been implicitly
// closed.
sendSimpleGetRequest(3);
handleGoAwayResponse(5);
}
@Test
public void testExceedMaxActiveStreams() throws Exception {
// http2Connect() - modified
enableHttp2(1);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
// validateHttp2InitialResponse() - modified
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[1]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
getSimpleResponseTrace(1)
, output.getTrace());
output.clearTrace();
sendLargeGetRequest(3);
sendSimpleGetRequest(5);
// Default connection window size is 64k-1.
// Initial request will have used 8k leaving 56k-1.
// Stream window will be 64k-1.
// Expecting
// 1 * headers
// 56k-1 of body (7 * ~8k)
// 1 * error (could be in any order)
for (int i = 0; i < 8; i++) {
parser.readFrame(true);
}
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().contains("5-RST-[" +
Http2Error.REFUSED_STREAM.getCode() + "]"));
output.clearTrace();
// Connection window is zero.
// Stream window is 8k
// Release the remaining body
sendWindowUpdate(0, (1 << 31) - 2);
// Allow for the 8k still in the stream window
sendWindowUpdate(3, (1 << 31) - 8193);
// 192k of body (24 * 8k)
// 1 * error (could be in any order)
for (int i = 0; i < 24; i++) {
parser.readFrame(true);
}
}
@Test
public void testErrorOnWaitingStream() throws Exception {
// http2Connect() - modified
enableHttp2(1);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
// validateHttp2InitialResponse() - modified
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[1]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
getSimpleResponseTrace(1)
, output.getTrace());
output.clearTrace();
sendLargeGetRequest(3);
sendSimpleGetRequest(5);
// Default connection window size is 64k-1.
// Initial request will have used 8k leaving 56k-1.
// Stream window will be 64k-1.
// Expecting
// 1 * headers
// 56k-1 of body (7 * ~8k)
// 1 * error (could be in any order)
for (int i = 0; i < 8; i++) {
parser.readFrame(true);
}
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().contains("5-RST-[" +
Http2Error.REFUSED_STREAM.getCode() + "]"));
output.clearTrace();
// Connection window is zero.
// Stream window is 8k
// Expand the stream window too much to trigger an error
// Allow for the 8k still in the stream window
sendWindowUpdate(3, (1 << 31) - 1);
parser.readFrame(true);
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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 org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Unit tests for Section 5.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_2 extends Http2TestBase {
/*
* Get the connection to a point where 1k of 8k response body has been
* read and the flow control for the stream has no capacity left.
*/
@Override
@Before
public void setUp() throws Exception {
super.setUp();
http2Connect();
// This test uses small window updates that will trigger the excessive
// overhead protection so disable it.
http2Protocol.setOverheadWindowUpdateThreshold(0);
// Set the default window size to 1024 bytes
sendSettings(0, false, new SettingValue(4, 1024));
// Wait for the ack
parser.readFrame(true);
output.clearTrace();
// Headers + 8k response
sendSimpleGetRequest(3);
// Headers
parser.readFrame(true);
// First 1k of body
parser.readFrame(true);
output.clearTrace();
}
@Test
public void testFlowControlLimits01() throws Exception {
readBytes(20);
clearRemainder();
}
@Test
public void testFlowControlLimits02() throws Exception {
readBytes(1);
readBytes(1);
readBytes(1024);
readBytes(1);
clearRemainder();
}
@Test
public void testFlowControlLimits03() throws Exception {
readBytes(8192,7168);
}
@Test
public void testFlowControlLimits04() throws Exception {
readBytes(7168, 7168, true);
}
private void readBytes(int len) throws Exception {
readBytes(len, len);
}
private void readBytes(int len, int expected) throws Exception {
readBytes(len, expected, len > expected);
}
private void readBytes(int len, int expected, boolean eos) throws Exception {
sendWindowUpdate(3, len);
parser.readFrame(true);
String expectedTrace = "3-Body-" + expected + "\n";
if (eos) {
expectedTrace += "3-EndOfStream\n";
}
Assert.assertEquals(expectedTrace, output.getTrace());
output.clearTrace();
}
private void clearRemainder() throws Exception {
// Remainder
sendWindowUpdate(3, 8192);
parser.readFrame(true);
}
}

View File

@@ -0,0 +1,249 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 5.3 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*
* Note: Unit tests for the examples described by each of the figures may be
* found in {@link TestAbstractStream}.
*/
public class TestHttp2Section_5_3 extends Http2TestBase {
// Section 5.3.1
@Test
public void testStreamDependsOnSelf() throws Exception {
http2Connect();
sendPriority(3, 3, 15);
parser.readFrame(true);
Assert.assertEquals("3-RST-[1]\n", output.getTrace());
}
// Section 5.3.2
@Test
public void testWeighting() throws Exception {
http2Connect();
// This test uses small window updates that will trigger the excessive
// overhead protection so disable it.
http2Protocol.setOverheadWindowUpdateThreshold(0);
// Default connection window size is 64k - 1. Initial request will have
// used 8k (56k -1). Increase it to 57k
sendWindowUpdate(0, 1 + 1024);
// Use up 56k of the connection window
for (int i = 3; i < 17; i += 2) {
sendSimpleGetRequest(i);
readSimpleGetResponse();
}
// Set the default window size to 1024 bytes
sendSettings(0, false, new SettingValue(4, 1024));
// Wait for the ack
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
output.clearTrace();
// At this point the connection window should be 1k and any new stream
// should have a window of 1k as well
// Set up streams A=17, B=19, C=21
sendPriority(17, 0, 15);
sendPriority(19, 17, 3);
sendPriority(21, 17, 11);
// First, process a request on stream 17. This should consume both
// stream 17's window and the connection window.
sendSimpleGetRequest(17);
// 17-headers, 17-1k-body
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
output.clearTrace();
// Send additional requests. Connection window is empty so only headers
// will be returned.
sendSimpleGetRequest(19);
sendSimpleGetRequest(21);
// Open up the flow control windows for stream 19 & 21 to more than the
// size of a simple request (8k)
sendWindowUpdate(19, 16*1024);
sendWindowUpdate(21, 16*1024);
// Read some frames
// 19-headers, 21-headers
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
output.clearTrace();
// At this point 17 is blocked because the stream window is zero and
// 19 & 21 are blocked because the connection window is zero.
//
// To test allocation, the connection window size is increased by 1.
// This should result in an allocation of 1 byte each to streams 19 and
// 21 but because each stream is processed in a separate thread it is
// not guaranteed that both streams will be blocked when the connection
// window size is increased. The test therefore sends 1 byte window
// updates until a small body has been seen from each stream. Then the
// tests sends a larger (1024 byte) window update and checks that it is
// correctly distributed between the streams.
//
// The test includes a margin to allow for the potential differences in
// response caused by timing differences on the server.
//
// The loop below handles 0, 1 or 2 stream being blocked
// - If 0 streams are blocked the connection window will be set to one
// and that will be consumed by the first stream to attempt to write.
// That body frame will be read by the client. The stream will then be
// blocked and the loop will start again.
// - If 1 stream is blocked, the connection window will be set to one
// which will then be consumed by the blocked stream. After writing
// the single byte the stream will again be blocked and the loop will
// start again.
// - If 2 streams are blocked the connection window will be set to one
// but one byte will be permitted for both streams (due to rounding in
// the allocation). The window size should be -1 (see below). Two
// frames (one for each stream will be written) one of which will be
// consumed by the client. The loop will start again and the Window
// size incremented to zero. No data will be written by the streams
// but the second data frame written in the last iteration of the loop
// will be read. The loop will then exit since frames from both
// streams will have been observed.
boolean seen19 = false;
boolean seen21 = false;
while (!seen19 || !seen21) {
sendWindowUpdate(0, 1);
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
int[] data = parseBodyFrame(output.getTrace());
if (data[0] == 19) {
seen19 = true;
} else if (data[0] == 21) {
seen21 = true;
} else {
// Unexpected stream
Assert.fail("Unexpected stream: [" + output.getTrace() + "]");
}
// A value of more than 1 here is unlikely but possible depending on
// how threads are scheduled. This has been observed as high as 12
// on ci.apache.org so allow a margin and use 20.
if (data[1] > 20) {
// Larger than expected body size
Assert.fail("Larger than expected body: [" + output.getTrace() + "]");
}
output.clearTrace();
}
sendWindowUpdate(0, 1024);
parser.readFrame(true);
// Make sure you have read the big comment before the loop above. It is
// possible that the timing of the server threads is such that there are
// still small body frames to read.
int[] data = parseBodyFrame(output.getTrace());
while (data[1] < 20) {
// Debugging Gump failure
log.info(output.getTrace());
output.clearTrace();
parser.readFrame(true);
data = parseBodyFrame(output.getTrace());
}
// Should now have two larger body frames. One has already been read.
seen19 = false;
seen21 = false;
while (!seen19 && !seen21) {
// Debugging Gump failure
log.info(output.getTrace());
if (data[0] == 19) {
seen19 = true;
// If everything works instantly this should be 256 but allow a
// fairly large margin for timing differences
if (data[1] < 216 || data[1] > 296) {
Assert.fail("Unexpected body size: [" + output.getTrace() + "]");
}
} else if (data[0] == 21) {
seen21 = true;
// If everything works instantly this should be 768 but allow a
// fairly large margin for timing differences
if (data[1] < 728 || data[1] > 808) {
Assert.fail("Unexpected body size: [" + output.getTrace() + "]");
}
} else {
Assert.fail("Unexpected stream: [" + output.getTrace() + "]");
}
output.clearTrace();
parser.readFrame(true);
data = parseBodyFrame(output.getTrace());
}
// Debugging Gump failure
log.info(output.getTrace());
output.clearTrace();
// Release everything and read all the remaining data
sendWindowUpdate(0, 1024 * 1024);
sendWindowUpdate(17, 1024 * 1024);
// Read remaining frames
// 17-7k-body, 19~8k-body, 21~8k-body
for (int i = 0; i < 3; i++) {
parser.readFrame(true);
// Debugging Gump failure
log.info(output.getTrace());
}
}
private int[] parseBodyFrame(String output) {
String[] parts = output.trim().split("-");
if (parts.length != 3 || !"Body".equals(parts[1])) {
Assert.fail("Unexpected output: [" + output + "]");
}
int[] result = new int[2];
result[0] = Integer.parseInt(parts[0]);
result[1] = Integer.parseInt(parts[2]);
return result;
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 5.5 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_5 extends Http2TestBase {
private static final byte[] UNKNOWN_FRAME;
static {
// Unknown frame type
UNKNOWN_FRAME = new byte[29];
// Frame header
ByteUtil.setThreeBytes(UNKNOWN_FRAME, 0, 20);
// Type
UNKNOWN_FRAME[3] = (byte) 0x80;
// No flags
// Stream
ByteUtil.set31Bits(UNKNOWN_FRAME, 5, 5);
// zero payload
}
// Section 5.5
@Test
public void testUnknownSetting() throws Exception {
http2Connect();
// Unknown setting (should be ack'd)
sendSettings(0, false, new SettingValue(1 << 15, 0));
parser.readFrame(true);
Assert.assertEquals("0-Settings-Ack\n", output.getTrace());
}
@Test
public void testUnknownFrame() throws Exception {
http2Connect();
os.write(UNKNOWN_FRAME);
os.flush();
// Ping
sendPing();
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[0,0,0,0,0,0,0,0]\n", output.getTrace());
}
@Test
public void testNonContiguousHeaderWithUnknownFrame() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
os.write(UNKNOWN_FRAME);
os.flush();
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_1 extends Http2TestBase {
@Test
public void testDataFrame() throws Exception {
http2Connect();
sendSimplePostRequest(3, null);
readSimplePostResponse(false);
Assert.assertEquals("0-WindowSize-[128]\n" +
"3-WindowSize-[128]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[128]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-128\n" +
"3-EndOfStream\n", output.getTrace());
}
@Test
public void testDataFrameWithPadding() throws Exception {
http2Connect();
byte[] padding = new byte[8];
sendSimplePostRequest(3, padding);
readSimplePostResponse(true);
// The window update for the padding could occur anywhere since it
// happens on a different thead to the response.
String trace = output.getTrace();
String paddingWindowUpdate = "0-WindowSize-[9]\n3-WindowSize-[9]\n";
Assert.assertTrue(trace, trace.contains(paddingWindowUpdate));
trace = trace.replace(paddingWindowUpdate, "");
Assert.assertEquals("0-WindowSize-[119]\n" +
"3-WindowSize-[119]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[119]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-119\n" +
"3-EndOfStream\n", trace);
}
@Test
public void testDataFrameWithNonZeroPadding() throws Exception {
http2Connect();
byte[] padding = new byte[8];
padding[4] = 0x01;
sendSimplePostRequest(3, padding);
// May see Window updates depending on timing
skipWindowSizeFrames();
String trace = output.getTrace();
Assert.assertTrue(trace, trace.startsWith("0-Goaway-[3]-[1]-["));
}
@Test
public void testDataFrameOnStreamZero() throws Exception {
http2Connect();
byte[] dataFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(dataFrame, 0, 1);
// type (0 for data)
// flags (0)
// stream (0)
// payload (0)
os.write(dataFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testDataFrameTooMuchPadding() throws Exception {
http2Connect();
byte[] dataFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(dataFrame, 0, 1);
// type 0 (data)
// flags 8 (padded)
dataFrame[4] = 0x08;
// stream 3
ByteUtil.set31Bits(dataFrame, 5, 3);
// payload (pad length of 1)
dataFrame[9] = 1;
os.write(dataFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testDataFrameWithZeroLengthPadding() throws Exception {
http2Connect();
byte[] padding = new byte[0];
sendSimplePostRequest(3, padding);
// Since padding is zero length, response looks like there is none.
readSimplePostResponse(false);
Assert.assertEquals("0-WindowSize-[127]\n" +
"3-WindowSize-[127]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[127]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-127\n" +
"3-EndOfStream\n", output.getTrace());
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_2 extends Http2TestBase {
@Test
public void testHeaderFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 0);
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameWithPadding() throws Exception {
http2Connect();
byte[] padding= new byte[8];
sendSimpleGetRequest(3, padding);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testHeaderFrameWithNonZeroPadding() throws Exception {
http2Connect();
byte[] padding= new byte[8];
padding[4] = 1;
sendSimpleGetRequest(3, padding);
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameTooMuchPadding() throws Exception {
http2Connect();
byte[] headerFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(headerFrame, 0, 1);
headerFrame[3] = FrameType.HEADERS.getIdByte();
// flags 8 (padded)
headerFrame[4] = 0x08;
// stream 3
ByteUtil.set31Bits(headerFrame, 5, 3);
// payload (pad length of 1)
headerFrame[9] = 1;
os.write(headerFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameWithZeroLengthPadding() throws Exception {
http2Connect();
byte[] padding= new byte[0];
sendSimpleGetRequest(3, padding);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.3 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_3 extends Http2TestBase {
@Test
public void testPriorityFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(0, 1, 15);
handleGoAwayResponse(1);
}
@Test
public void testPriorityFrameBetweenHeaderFrames() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
sendPriority(5, 3, 15);
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
@Test
public void testPriorityFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] priorityFrame = new byte[10];
// length
ByteUtil.setThreeBytes(priorityFrame, 0, 1);
// type
priorityFrame[3] = FrameType.PRIORITY.getIdByte();
// No flags
// Stream ID
ByteUtil.set31Bits(priorityFrame, 5, 3);
// Payload - left as zero
os.write(priorityFrame);
os.flush();
// Read reset frame
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n",
output.getTrace());
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.4 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_4 extends Http2TestBase {
@Test
public void testResetFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
sendRst(0, Http2Error.NO_ERROR.getCode());
handleGoAwayResponse(1);
}
@Test
public void testResetFrameOnIdleStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(3, 0, 15);
sendRst(3, Http2Error.NO_ERROR.getCode());
handleGoAwayResponse(1);
}
@Test
public void testResetFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] resetFrame = new byte[10];
// length
ByteUtil.setThreeBytes(resetFrame, 0, 1);
// type
resetFrame[3] = FrameType.RST.getIdByte();
// No flags
// Stream ID
ByteUtil.set31Bits(resetFrame, 5, 3);
// Payload - left as zero
os.write(resetFrame);
os.flush();
// Read reset frame
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n",
output.getTrace());
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.5 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_5 extends Http2TestBase {
@Test
public void testSettingsFrameNonEmptAck() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, true, new SettingValue(1,1));
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testSettingsFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(3, 0, 15);
sendSettings(3, true, new SettingValue(1,1));
handleGoAwayResponse(1);
}
@Test
public void testSettingsFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] resetFrame = new byte[10];
// length
ByteUtil.setThreeBytes(resetFrame, 0, 1);
// type
resetFrame[3] = FrameType.SETTINGS.getIdByte();
// No flags
// Stream ID 0
// Payload - left as zero
os.write(resetFrame);
os.flush();
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
// Need to test sending push promise when push promise support is disabled
@Test
public void testSettingsFrameInvalidPushSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x2,0x2));
handleGoAwayResponse(1);
}
@Test
public void testSettingsFrameInvalidWindowSizeSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x4,1 << 31));
handleGoAwayResponse(1, Http2Error.FLOW_CONTROL_ERROR);
}
@Test
public void testSettingsFrameInvalidMaxFrameSizeSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x5,1 << 31));
handleGoAwayResponse(1);
}
@Test
public void testSettingsUnknownSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0xFF,0xFF));
// Ack
parser.readFrame(true);
Assert.assertTrue(output.getTrace(), output.getTrace().startsWith(
"0-Settings-Ack"));
}
// delayed ACKs. Requires an API (TBD) for applications to send settings.
}

View File

@@ -0,0 +1,83 @@
/*
* 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.nio.charset.StandardCharsets;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.7 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_7 extends Http2TestBase {
@Test
public void testPingFrame() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, false, "01234567".getBytes(StandardCharsets.ISO_8859_1));
// Ping ack
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[48,49,50,51,52,53,54,55]\n", output.getTrace());
}
@Test
public void testPingFrameUnexpectedAck() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, true, "01234567".getBytes(StandardCharsets.ISO_8859_1));
sendPing(0, false, "76543210".getBytes(StandardCharsets.ISO_8859_1));
// Ping ack (only for second ping)
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[55,54,53,52,51,50,49,48]\n", output.getTrace());
}
@Test
public void testPingFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(1, false, "76543210".getBytes(StandardCharsets.ISO_8859_1));
handleGoAwayResponse(1);
}
@Test
public void testPingFrameWrongPayloadSize() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, false, "6543210".getBytes(StandardCharsets.ISO_8859_1));
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.8 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_8 extends Http2TestBase {
private static final boolean RELAX_TIMING = Boolean.getBoolean("tomcat.test.relaxTiming");
private static final long PING_ACK_DELAY_MS = 2000;
// On slow systems (Gump) may need to be higher
private static final long TIMING_MARGIN_MS = RELAX_TIMING ? 1000 : 200;
@Test
public void testGoawayIgnoreNewStreams() throws Exception {
setPingAckDelayMillis(PING_ACK_DELAY_MS);
http2Connect();
http2Protocol.setMaxConcurrentStreams(200);
Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS);
getTomcatInstance().getConnector().pause();
// Go away
parser.readFrame(true);
Assert.assertEquals("0-Goaway-[2147483647]-[0]-[null]", output.getTrace());
output.clearTrace();
// Should be processed
sendSimpleGetRequest(3);
Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS);
// Should be ignored
sendSimpleGetRequest(5);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
output.clearTrace();
// Finally the go away frame
parser.readFrame(true);
Assert.assertEquals("0-Goaway-[3]-[0]-[null]", output.getTrace());
}
@Test
public void testGoawayFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendGoaway(1, 1, Http2Error.NO_ERROR.getCode(), null);
handleGoAwayResponse(1);
}
// TODO Test header processing and window size processing for ignored
// streams
}

View File

@@ -0,0 +1,282 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.9 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_9 extends Http2TestBase {
@Test
public void testZeroWindowUpdateConnection() throws Exception {
http2Connect();
sendWindowUpdate(0, 0);
handleGoAwayResponse(1);
}
@Test
public void testZeroWindowUpdateStream() throws Exception {
http2Connect();
sendSimplePostRequest(3, null, false);
sendWindowUpdate(3, 0);
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.PROTOCOL_ERROR.getCode() + "]\n",
output.getTrace());
}
@Test
public void testWindowUpdateOnClosedStream() throws Exception {
http2Connect();
// Should not be an error so should be nothing to read
sendWindowUpdate(1, 200);
// So the next request should process normally
sendSimpleGetRequest(3);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
// TODO: Test always accounting for changes in flow control windows even if
// the frame is in error.
@Test
public void testWindowUpdateWrongLength() throws Exception {
http2Connect();
byte[] zeroLengthWindowFrame = new byte[9];
// Length zero
ByteUtil.setOneBytes(zeroLengthWindowFrame, 3, FrameType.WINDOW_UPDATE.getIdByte());
// No flags
// Stream 1
ByteUtil.set31Bits(zeroLengthWindowFrame, 5, 1);
os.write(zeroLengthWindowFrame);
os.flush();
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testEmptyDataFrameWithNoAvailableFlowControl() throws Exception {
http2Connect();
// Default connection window size is 64k - 1. Initial request will have
// used 8k (56k -1).
// Use up the remaining connection window. These requests require 56k
// but there is only 56k - 1 available.
for (int i = 3; i < 17; i += 2) {
sendSimpleGetRequest(i);
readSimpleGetResponse();
}
output.clearTrace();
// It should be possible to send a request that generates an empty
// response at this point
sendEmptyGetRequest(17);
// Headers
parser.readFrame(true);
// Body
parser.readFrame(true);
// Release Stream 15 which is waiting for a single byte.
sendWindowUpdate(0, 1024);
Assert.assertEquals(getEmptyResponseTrace(17), output.getTrace());
}
@Test
public void testWindowSizeTooLargeStream() throws Exception {
http2Connect();
// Set up stream 3
sendSimplePostRequest(3, null, false);
// Super size the flow control window.
sendWindowUpdate(3, (1 << 31) - 1);
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]\n",
output.getTrace());
}
@Test
public void testWindowSizeTooLargeConnection() throws Exception {
http2Connect();
// Super size the flow control window.
sendWindowUpdate(0, (1 << 31) - 1);
handleGoAwayResponse(1, Http2Error.FLOW_CONTROL_ERROR);
}
@Test
public void testWindowSizeAndSettingsFrame() throws Exception {
http2Connect();
// Set up a POST request that echoes the body back
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(8 * 1024);
buildPostRequest(headersFrameHeader, headersPayload, false,
dataFrameHeader, dataPayload, null, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Now use a settings frame to reduce the size of the flow control
// window.
sendSettings(0, false, new SettingValue(4, 4 * 1024));
// Ack
parser.readFrame(true);
Assert.assertEquals("0-Settings-Ack\n", output.getTrace());
output.clearTrace();
// Write the body
writeFrame(dataFrameHeader, dataPayload);
// Window size updates after reading POST body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(
"0-WindowSize-[8192]\n" +
"3-WindowSize-[8192]\n",
output.getTrace());
output.clearTrace();
// Read stream 3 headers and first part of body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[date]-["+ DEFAULT_DATE + "]\n" +
"3-HeadersEnd\n" +
"3-Body-4096\n", output.getTrace());
output.clearTrace();
// Now use a settings frame to further reduce the size of the flow
// control window. This should make the stream 3 window negative
sendSettings(0, false, new SettingValue(4, 2 * 1024));
// Ack
parser.readFrame(true);
Assert.assertEquals("0-Settings-Ack\n", output.getTrace());
output.clearTrace();
// Now use a settings frame to increase the size of the flow control
// window. The stream 3 window should still be negative
sendSettings(0, false, new SettingValue(4, 3 * 1024));
// Ack
parser.readFrame(true);
Assert.assertEquals("0-Settings-Ack\n", output.getTrace());
output.clearTrace();
// Do a POST that won't be affected by the above limit
sendSimplePostRequest(5, null);
// Window size updates after reading POST body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(
"0-WindowSize-[128]\n" +
"5-WindowSize-[128]\n",
output.getTrace());
output.clearTrace();
// Headers + body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(
"5-HeadersStart\n" +
"5-Header-[:status]-[200]\n" +
"5-Header-[content-length]-[128]\n" +
"5-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"5-HeadersEnd\n" +
"5-Body-128\n" +
"5-EndOfStream\n", output.getTrace());
output.clearTrace();
// Now use a settings frame to restore the size of the flow control
// window.
sendSettings(0, false, new SettingValue(4, 64 * 1024 - 1));
// Settings ack and stream 3 body are written from different threads.
// Order depends on server side timing. Handle both possibilities.
parser.readFrame(true);
String trace = output.getTrace();
String settingsAck = "0-Settings-Ack\n";
String endOfStreamThree = "3-Body-4096\n3-EndOfStream\n";
if (settingsAck.equals(trace)) {
// Ack the end of stream 3
output.clearTrace();
parser.readFrame(true);
Assert.assertEquals(endOfStreamThree, output.getTrace());
} else {
// End of stream 3 thenack
Assert.assertEquals(endOfStreamThree, output.getTrace());
output.clearTrace();
parser.readFrame(true);
Assert.assertEquals(settingsAck, output.getTrace());
}
output.clearTrace();
}
@Test
public void testWindowSizeTooLargeViaSettings() throws Exception {
http2Connect();
// Set up stream 3
sendSimplePostRequest(3, null, false);
// Increase the flow control window but keep it under the limit
sendWindowUpdate(3, 1 << 30);
// Now increase beyond the limit via a settings frame
sendSettings(0, false, new SettingValue(4, 1 << 30));
// Ack
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FLOW_CONTROL_ERROR.getCode() + "]\n",
output.getTrace());
}
}

View File

@@ -0,0 +1,217 @@
/*
* 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.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 8.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* examples in the RFC.
*/
public class TestHttp2Section_8_1 extends Http2TestBase {
@Test
public void testPostWithTrailerHeaders() throws Exception {
doTestPostWithTrailerHeaders(true);
}
@Test
public void testPostWithTrailerHeadersBlocked() throws Exception {
doTestPostWithTrailerHeaders(false);
}
private void doTestPostWithTrailerHeaders(boolean allowTrailerHeader) throws Exception{
http2Connect();
if (allowTrailerHeader) {
http2Protocol.setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
}
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, trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
writeFrame(dataFrameHeader, dataPayload);
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
String len;
if (allowTrailerHeader) {
len = Integer.toString(256 + TRAILER_HEADER_VALUE.length());
} else {
len = "256";
}
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());
}
@Test
public void testSendAck() throws Exception {
http2Connect();
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, true,
dataFrameHeader, dataPayload, null, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-HeadersStart\n" +
"3-Header-[:status]-[100]\n" +
"3-HeadersEnd\n",
output.getTrace());
output.clearTrace();
// Write the body
writeFrame(dataFrameHeader, dataPayload);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-WindowSize-[256]\n" +
"3-WindowSize-[256]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[256]\n" +
"3-Header-[date]-["+ DEFAULT_DATE + "]\n" +
"3-HeadersEnd\n" +
"3-Body-256\n" +
"3-EndOfStream\n",
output.getTrace());
}
@Test
public void testUndefinedPseudoHeader() throws Exception {
List<Header> headers = new ArrayList<>(5);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header(":foo", "bar"));
doInvalidPseudoHeaderTest(headers);
}
@Test
public void testInvalidPseudoHeader() throws Exception {
List<Header> headers = new ArrayList<>(5);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header(":status", "200"));
doInvalidPseudoHeaderTest(headers);
}
@Test
public void testPseudoHeaderOrder() throws Exception {
// Need to do this in two frames because HPACK encoder automatically
// re-orders fields
http2Connect();
List<Header> headers = new ArrayList<>(4);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header("x-test", "test"));
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(headersFrameHeader, headersPayload, headers , 3);
writeFrame(headersFrameHeader, headersPayload);
headers.clear();
headers.add(new Header(":authority", "localhost:" + getPort()));
headersPayload.clear();
buildSimpleGetRequestPart2(headersFrameHeader, headersPayload, headers , 3);
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-RST-[1]\n", output.getTrace());
}
private void doInvalidPseudoHeaderTest(List<Header> headers) throws Exception {
http2Connect();
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(headersFrameHeader, headersPayload, null, headers , 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-RST-[1]\n", output.getTrace());
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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 org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class TestHttp2Timeouts extends Http2TestBase {
@Override
@Before
public void http2Connect() throws Exception {
super.http2Connect();
}
/*
* Simple request won't fill buffer so timeout will occur in Tomcat internal
* code during response completion.
*/
@Test
public void testClientWithEmptyWindow() throws Exception {
sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0));
sendSimpleGetRequest(3);
// Settings
parser.readFrame(false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Large request will fill buffer so timeout will occur in application code
* during response write (when Tomcat commits the response and flushes the
* buffer as a result of the buffer filling).
*/
@Test
public void testClientWithEmptyWindowLargeResponse() throws Exception {
sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0));
sendLargeGetRequest(3);
// Settings
parser.readFrame(false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Timeout with app reading request body directly.
*/
@Test
public void testClientPostsNoBody() throws Exception {
sendSimplePostRequest(3, null, false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Timeout with app processing parameters.
*/
@Test
public void testClientPostsNoParameters() throws Exception {
sendParameterPostRequest(3, null, null, 10, false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public class TestHttp2UpgradeHandler extends Http2TestBase {
// https://bz.apache.org/bugzilla/show_bug.cgi?id=60970
@Test
public void testLargeHeader() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Tomcat.addServlet(ctxt, "large", new LargeHeaderServlet());
ctxt.addServletMappingDecoded("/large", "large");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/large");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
parser.readFrame(true);
// Body
parser.readFrame(true);
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[x-ignore]-[...]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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 javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public class TestStream extends Http2TestBase {
/*
* https://bz.apache.org/bugzilla/show_bug.cgi?id=61120
*/
@Test
public void testPathParam() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Tomcat.addServlet(ctxt, "pathparam", new PathParam());
ctxt.addServletMappingDecoded("/pathparam", "pathparam");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3,
"/pathparam;jsessionid=" + PathParam.EXPECTED_SESSION_ID);
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
private static final class PathParam extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final String EXPECTED_SESSION_ID = "0123456789ABCDEF";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
if (EXPECTED_SESSION_ID.equals(request.getRequestedSessionId())) {
response.getWriter().write("OK");
} else {
response.getWriter().write("FAIL");
}
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* 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.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.compat.JrePlatform;
import org.apache.tomcat.util.http.FastHttpDateFormat;
public class TestStreamProcessor extends Http2TestBase {
@Test
public void testAsyncComplete() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
// Map the async servlet to /simple so we can re-use the HTTP/2 handling
// logic from the super class.
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncComplete());
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
// Flush before startAsync means body is written in two packets so an
// additional frame needs to be read
parser.readFrame(true);
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-17\n" +
"3-Body-8\n" +
"3-EndOfStream\n", output.getTrace());
}
@Test
public void testAsyncDispatch() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
// Map the async servlet to /simple so we can re-use the HTTP/2 handling
// logic from the super class.
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncDispatch());
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testPrepareHeaders() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
File appDir = new File("test/webapp");
Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
List<Header> headers = new ArrayList<>(3);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/index.html"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header("if-modified-since", FastHttpDateFormat.getCurrentDate()));
buildGetRequest(frameHeader, headersPayload, null, headers, 3);
writeFrame(frameHeader, headersPayload);
parser.readFrame(true);
StringBuilder expected = new StringBuilder();
expected.append("3-HeadersStart\n");
expected.append("3-Header-[:status]-[304]\n");
// Different line-endings -> different files size -> different weak eTag
if (JrePlatform.IS_WINDOWS) {
expected.append("3-Header-[etag]-[W/\"957-1447269522000\"]\n");
} else {
expected.append("3-Header-[etag]-[W/\"934-1447269522000\"]\n");
}
expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
expected.append("3-HeadersEnd\n");
Assert.assertEquals(expected.toString(), output.getTrace());
}
private static final class AsyncComplete extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
PrintWriter pw = response.getWriter();
pw.print("Enter-");
final AsyncContext asyncContext = request.startAsync(request, response);
pw.print("StartAsync-");
pw.flush();
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
asyncContext.getResponse().getWriter().print("Complete");
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
private static final class AsyncDispatch extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
asyncContext.dispatch("/simple");
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* 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.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.buf.HexUtils;
/*
* See https://bz.apache.org/bugzilla/show_bug.cgi?id=60482
*/
@RunWith(Parameterized.class)
public class TestStreamQueryString extends Http2TestBase {
@Parameters
public static Collection<Object[]> inputs() {
List<Object[]> result = new ArrayList<>();
// Test ASCII characters from 32 to 126 inclusive
for (int i = 32; i < 128; i++) {
result.add(new String[] { "%" + HexUtils.toHexString(new byte[] { (byte) i})});
}
return result;
}
private final String queryValueToTest;
public TestStreamQueryString(String queryValueToTest) {
this.queryValueToTest = queryValueToTest;
}
@Test
public void testQueryString() throws Exception {
String queryValue = "xxx" + queryValueToTest + "xxx";
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "query", new Query(queryValue));
ctxt.addServletMappingDecoded("/query", "query");
tomcat.start();
openClientConnection();
doHttpUpgrade(queryValue);
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3,
"/query?" + Query.PARAM_NAME + "=" + queryValue);
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(queryValue,
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
protected void doHttpUpgrade(String queryValue) throws IOException {
byte[] upgradeRequest = ("GET /query?" + Query.PARAM_NAME + "=" + queryValue + " HTTP/1.1\r\n" +
"Host: localhost:" + getPort() + "\r\n" +
"Connection: "+ DEFAULT_CONNECTION_HEADER_VALUE + "\r\n" +
"Upgrade: h2c\r\n" +
EMPTY_HTTP2_SETTINGS_HEADER +
"\r\n").getBytes(StandardCharsets.ISO_8859_1);
os.write(upgradeRequest);
os.flush();
Assert.assertTrue("Failed to read HTTP Upgrade response",
readHttpUpgradeResponse());
}
@Override
protected void validateHttp2InitialResponse() throws Exception {
// - 101 response acts as acknowledgement of the HTTP2-Settings header
// Need to read 5 frames
// - settings (server settings - must be first)
// - settings ack (for the settings frame in the client preface)
// - ping
// - headers (for response)
// - data (for response body)
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[200]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
"1-HeadersStart\n" +
"1-Header-[:status]-[200]\n" +
"1-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"1-Header-[content-length]-[2]\n" +
"1-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"1-HeadersEnd\n" +
"1-Body-2\n" +
"1-EndOfStream\n", output.getTrace());
output.clearTrace();
}
private static final class Query extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String PARAM_NAME = "param";
private final String expectedValue;
public Query(String expectedValue) {
String decoded;
try {
decoded = URLDecoder.decode(expectedValue, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Can't happen with UTF-8
decoded = null;
}
this.expectedValue = decoded;
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
if (expectedValue.equals(request.getParameter(PARAM_NAME))) {
response.getWriter().write("OK");
} else {
response.getWriter().write("FAIL");
}
}
}
}