init
This commit is contained in:
86
test/org/apache/coyote/TestCompressionConfig.java
Normal file
86
test/org/apache/coyote/TestCompressionConfig.java
Normal 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class TestCompressionConfig {
|
||||
|
||||
@Parameterized.Parameters(name = "{index}: accept-encoding[{0}], ETag [{1}], NoCompressionStrongETag[{2}], compress[{3}]")
|
||||
public static Collection<Object[]> parameters() {
|
||||
List<Object[]> parameterSets = new ArrayList<>();
|
||||
|
||||
parameterSets.add(new Object[] { new String[] { }, null, Boolean.TRUE, Boolean.FALSE });
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.TRUE, Boolean.FALSE });
|
||||
parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.TRUE, Boolean.FALSE });
|
||||
parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE });
|
||||
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.FALSE });
|
||||
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.FALSE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.FALSE, Boolean.TRUE });
|
||||
parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.TRUE });
|
||||
|
||||
return parameterSets;
|
||||
}
|
||||
|
||||
@Parameter(0)
|
||||
public String[] headers;
|
||||
@Parameter(1)
|
||||
public String eTag;
|
||||
@Parameter(2)
|
||||
public Boolean noCompressionStrongETag;
|
||||
@Parameter(3)
|
||||
public Boolean compress;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void testUseCompression() throws Exception {
|
||||
|
||||
CompressionConfig compressionConfig = new CompressionConfig();
|
||||
// Skip length and MIME type checks
|
||||
compressionConfig.setCompression("force");
|
||||
compressionConfig.setNoCompressionStrongETag(noCompressionStrongETag.booleanValue());
|
||||
|
||||
Request request = new Request();
|
||||
Response response = new Response();
|
||||
|
||||
for (String header : headers) {
|
||||
request.getMimeHeaders().addValue("accept-encoding").setString(header);
|
||||
}
|
||||
|
||||
if (eTag != null) {
|
||||
response.getMimeHeaders().addValue("ETag").setString(eTag);
|
||||
}
|
||||
|
||||
|
||||
Assert.assertEquals(compress, Boolean.valueOf(compressionConfig.useCompression(request, response)));
|
||||
}
|
||||
}
|
||||
241
test/org/apache/coyote/TestIoTimeouts.java
Normal file
241
test/org/apache/coyote/TestIoTimeouts.java
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
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.SimpleHttpClient;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
|
||||
public class TestIoTimeouts extends TomcatBaseTest {
|
||||
|
||||
@Test
|
||||
public void testNonBlockingReadWithNoTimeout() {
|
||||
// Sends complete request in 3 packets
|
||||
ChunkedClient client = new ChunkedClient(true);
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
Assert.assertNull(EchoListener.t);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testNonBlockingReadTimeout() {
|
||||
// Sends incomplete request (no end chunk) so read times out
|
||||
ChunkedClient client = new ChunkedClient(false);
|
||||
client.doRequest();
|
||||
Assert.assertFalse(client.isResponse200());
|
||||
Assert.assertFalse(client.isResponseBodyOK());
|
||||
// Socket will be closed before the error handler runs. Closing the
|
||||
// socket triggers the client code's return from the doRequest() method
|
||||
// above so we need to wait at this point for the error handler to be
|
||||
// triggered.
|
||||
int count = 0;
|
||||
// Shouldn't need to wait long but allow plenty of time as the CI
|
||||
// systems are sometimes slow.
|
||||
while (count < 100 && EchoListener.t == null) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
count++;
|
||||
}
|
||||
Assert.assertNotNull(EchoListener.t);
|
||||
}
|
||||
|
||||
|
||||
private class ChunkedClient extends SimpleHttpClient {
|
||||
|
||||
private final boolean sendEndChunk;
|
||||
|
||||
|
||||
public ChunkedClient(boolean sendEndChunk) {
|
||||
this.sendEndChunk = sendEndChunk;
|
||||
}
|
||||
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Wrapper w = Tomcat.addServlet(root, "Test", new NonBlockingEchoServlet());
|
||||
w.setAsyncSupported(true);
|
||||
root.addServletMappingDecoded("/test", "Test");
|
||||
|
||||
try {
|
||||
tomcat.start();
|
||||
setPort(tomcat.getConnector().getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
int packetCount = 2;
|
||||
if (sendEndChunk) {
|
||||
packetCount++;
|
||||
}
|
||||
|
||||
String[] request = new String[packetCount];
|
||||
request[0] =
|
||||
"POST /test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"Transfer-Encoding: chunked" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
request[1] =
|
||||
"b8" + CRLF +
|
||||
"{" + CRLF +
|
||||
" \"tenantId\": \"dotCom\", " + CRLF +
|
||||
" \"locale\": \"en-US\", " + CRLF +
|
||||
" \"defaultZoneId\": \"25\", " + CRLF +
|
||||
" \"itemIds\": [\"StaplesUSCAS/en-US/2/<EOF>/<EOF>\"] , " + CRLF +
|
||||
" \"assetStoreId\": \"5051\", " + CRLF +
|
||||
" \"zipCode\": \"98109\"" + CRLF +
|
||||
"}" + CRLF;
|
||||
if (sendEndChunk) {
|
||||
request[2] =
|
||||
"0" + CRLF +
|
||||
CRLF;
|
||||
}
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("98109")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class NonBlockingEchoServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// Need to be in async mode to use non-blocking I/O
|
||||
AsyncContext ac = req.startAsync();
|
||||
ac.setTimeout(10000);
|
||||
|
||||
ServletInputStream sis = null;
|
||||
ServletOutputStream sos = null;
|
||||
|
||||
try {
|
||||
sis = req.getInputStream();
|
||||
sos = resp.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new ServletException(ioe);
|
||||
}
|
||||
|
||||
EchoListener listener = new EchoListener(ac, sis, sos);
|
||||
sis.setReadListener(listener);
|
||||
sos.setWriteListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class EchoListener implements ReadListener, WriteListener {
|
||||
|
||||
private static volatile Throwable t;
|
||||
|
||||
private final AsyncContext ac;
|
||||
private final ServletInputStream sis;
|
||||
private final ServletOutputStream sos;
|
||||
private final byte[] buffer = new byte[8192];
|
||||
|
||||
public EchoListener(AsyncContext ac, ServletInputStream sis, ServletOutputStream sos) {
|
||||
t = null;
|
||||
this.ac = ac;
|
||||
this.sis = sis;
|
||||
this.sos = sos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWritePossible() throws IOException {
|
||||
if (sis.isFinished()) {
|
||||
sos.flush();
|
||||
ac.complete();
|
||||
return;
|
||||
}
|
||||
while (sis.isReady()) {
|
||||
int read = sis.read(buffer);
|
||||
if (read > 0) {
|
||||
sos.write(buffer, 0, read);
|
||||
if (!sos.isReady()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException {
|
||||
if (sos.isReady()) {
|
||||
onWritePossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException {
|
||||
if (sos.isReady()) {
|
||||
onWritePossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
t = throwable;
|
||||
ac.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
148
test/org/apache/coyote/TestResponse.java
Normal file
148
test/org/apache/coyote/TestResponse.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
import org.apache.tomcat.util.buf.ByteChunk;
|
||||
|
||||
public class TestResponse extends TomcatBaseTest {
|
||||
|
||||
/*
|
||||
* https://bz.apache.org/bugzilla/show_bug.cgi?id=61197
|
||||
*/
|
||||
@Test
|
||||
public void testUserCharsetIsRetained() throws Exception {
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
// Add servlet
|
||||
Tomcat.addServlet(ctx, "CharsetServlet", new CharsetServlet());
|
||||
ctx.addServletMappingDecoded("/*", "CharsetServlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
ByteChunk responseBody = new ByteChunk();
|
||||
Map<String,List<String>> responseHeaders = new HashMap<>();
|
||||
int rc = getUrl("http://localhost:" + getPort() + "/test?charset=uTf-8", responseBody,
|
||||
responseHeaders);
|
||||
|
||||
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
|
||||
|
||||
String contentType = getSingleHeader("Content-Type", responseHeaders);
|
||||
Assert.assertEquals("text/plain;charset=uTf-8", contentType);
|
||||
}
|
||||
|
||||
|
||||
private static class CharsetServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
resp.setContentType("text/plain");
|
||||
resp.setCharacterEncoding(req.getParameter("charset"));
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testContentTypeWithSpace() throws Exception {
|
||||
doTestContentTypeSpacing(true);
|
||||
}
|
||||
|
||||
|
||||
@Ignore // Disabled until Bug 62912 is addressed
|
||||
@Test
|
||||
public void testContentTypeWithoutSpace() throws Exception {
|
||||
doTestContentTypeSpacing(false);
|
||||
}
|
||||
|
||||
|
||||
private void doTestContentTypeSpacing(boolean withSpace) throws Exception {
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
// Add servlet
|
||||
Tomcat.addServlet(ctx, "ContentTypeServlet", new ContentTypeServlet());
|
||||
ctx.addServletMappingDecoded("/*", "ContentTypeServlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
ByteChunk responseBody = new ByteChunk();
|
||||
Map<String,List<String>> responseHeaders = new HashMap<>();
|
||||
StringBuilder uri = new StringBuilder("http://localhost:");
|
||||
uri.append(getPort());
|
||||
uri.append("/test");
|
||||
if (withSpace) {
|
||||
uri.append("?withSpace=true");
|
||||
}
|
||||
int rc = getUrl(uri.toString(), responseBody, responseHeaders);
|
||||
|
||||
Assert.assertEquals(HttpServletResponse.SC_OK, rc);
|
||||
|
||||
String contentType = getSingleHeader("Content-Type", responseHeaders);
|
||||
StringBuilder expected = new StringBuilder("text/plain;");
|
||||
if (withSpace) {
|
||||
expected.append(" ");
|
||||
}
|
||||
expected.append("v=1;charset=UTF-8");
|
||||
Assert.assertEquals(expected.toString() , contentType);
|
||||
}
|
||||
|
||||
|
||||
private static class ContentTypeServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (req.getParameter("withSpace") == null) {
|
||||
resp.setContentType("text/plain;v=1");
|
||||
} else {
|
||||
resp.setContentType("text/plain; v=1");
|
||||
}
|
||||
resp.setCharacterEncoding("UTF-8");
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
426
test/org/apache/coyote/ajp/SimpleAjpClient.java
Normal file
426
test/org/apache/coyote/ajp/SimpleAjpClient.java
Normal file
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* 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.ajp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
/**
|
||||
* AJP client that is not (yet) a full AJP client implementation as it just
|
||||
* provides the functionality required for the unit tests. The client uses
|
||||
* blocking IO throughout.
|
||||
*/
|
||||
public class SimpleAjpClient {
|
||||
|
||||
private static final int DEFAULT_AJP_PACKET_SIZE = 8192;
|
||||
private static final byte[] AJP_CPING;
|
||||
|
||||
static {
|
||||
TesterAjpMessage ajpCping = new TesterAjpMessage(16);
|
||||
ajpCping.reset();
|
||||
ajpCping.appendByte(Constants.JK_AJP13_CPING_REQUEST);
|
||||
ajpCping.end();
|
||||
AJP_CPING = new byte[ajpCping.getLen()];
|
||||
System.arraycopy(ajpCping.getBuffer(), 0, AJP_CPING, 0,
|
||||
ajpCping.getLen());
|
||||
}
|
||||
|
||||
private final int packetSize;
|
||||
private String host = "localhost";
|
||||
private int port = -1;
|
||||
/* GET == 2 */
|
||||
private int method = 2;
|
||||
private String protocol = "http";
|
||||
private String uri = "/";
|
||||
private String remoteAddr = "192.168.0.1";
|
||||
private String remoteHost = "client.example.com";
|
||||
private String serverName = "www.example.com";
|
||||
private int serverPort = 80;
|
||||
private boolean ssl = false;
|
||||
private Socket socket = null;
|
||||
|
||||
public SimpleAjpClient() {
|
||||
this(DEFAULT_AJP_PACKET_SIZE);
|
||||
}
|
||||
|
||||
public SimpleAjpClient(int packetSize) {
|
||||
this.packetSize = packetSize;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
method = method.toUpperCase(Locale.ENGLISH);
|
||||
switch (method) {
|
||||
case "OPTIONS":
|
||||
this.method = 1;
|
||||
break;
|
||||
case "GET":
|
||||
this.method = 2;
|
||||
break;
|
||||
case "HEAD":
|
||||
this.method = 3;
|
||||
break;
|
||||
case "POST":
|
||||
this.method = 4;
|
||||
break;
|
||||
case "PUT":
|
||||
this.method = 5;
|
||||
break;
|
||||
case "DELETE":
|
||||
this.method = 6;
|
||||
break;
|
||||
case "TRACE":
|
||||
this.method = 7;
|
||||
break;
|
||||
case "PROPFIND":
|
||||
this.method = 8;
|
||||
break;
|
||||
case "PROPPATCH":
|
||||
this.method = 9;
|
||||
break;
|
||||
case "MKCOL":
|
||||
this.method = 10;
|
||||
break;
|
||||
case "COPY":
|
||||
this.method = 11;
|
||||
break;
|
||||
case "MOVE":
|
||||
this.method = 12;
|
||||
break;
|
||||
case "LOCK":
|
||||
this.method = 13;
|
||||
break;
|
||||
case "UNLOCK":
|
||||
this.method = 14;
|
||||
break;
|
||||
case "ACL":
|
||||
this.method = 15;
|
||||
break;
|
||||
case "REPORT":
|
||||
this.method = 16;
|
||||
break;
|
||||
case "VERSION-CONTROL":
|
||||
this.method = 17;
|
||||
break;
|
||||
case "CHECKIN":
|
||||
this.method = 18;
|
||||
break;
|
||||
case "CHECKOUT":
|
||||
this.method = 19;
|
||||
break;
|
||||
case "UNCHECKOUT":
|
||||
this.method = 20;
|
||||
break;
|
||||
case "SEARCH":
|
||||
this.method = 21;
|
||||
break;
|
||||
case "MKWORKSPACE":
|
||||
this.method = 22;
|
||||
break;
|
||||
case "UPDATE":
|
||||
this.method = 23;
|
||||
break;
|
||||
case "LABEL":
|
||||
this.method = 24;
|
||||
break;
|
||||
case "MERGE":
|
||||
this.method = 25;
|
||||
break;
|
||||
case "BASELINE-CONTROL":
|
||||
this.method = 26;
|
||||
break;
|
||||
case "MKACTIVITY":
|
||||
this.method = 27;
|
||||
break;
|
||||
default:
|
||||
this.method = 99;
|
||||
}
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
switch (method) {
|
||||
case 1:
|
||||
return "OPTIONS";
|
||||
case 2:
|
||||
return "GET";
|
||||
case 3:
|
||||
return "HEAD";
|
||||
case 4:
|
||||
return "POST";
|
||||
case 5:
|
||||
return "PUT";
|
||||
case 6:
|
||||
return "DELETE";
|
||||
case 7:
|
||||
return "TRACE";
|
||||
case 8:
|
||||
return "PROPFIND";
|
||||
case 9:
|
||||
return "PROPPATCH";
|
||||
case 10:
|
||||
return "MKCOL";
|
||||
case 11:
|
||||
return "COPY";
|
||||
case 12:
|
||||
return "MOVE";
|
||||
case 13:
|
||||
return "LOCK";
|
||||
case 14:
|
||||
return "UNLOCK";
|
||||
case 15:
|
||||
return "ACL";
|
||||
case 16:
|
||||
return "REPORT";
|
||||
case 17:
|
||||
return "VERSION-CONTROL";
|
||||
case 18:
|
||||
return "CHECKIN";
|
||||
case 19:
|
||||
return "CHECKOUT";
|
||||
case 20:
|
||||
return "UNCHECKOUT";
|
||||
case 21:
|
||||
return "SEARCH";
|
||||
case 22:
|
||||
return "MKWORKSPACE";
|
||||
case 23:
|
||||
return "UPDATE";
|
||||
case 24:
|
||||
return "LABEL";
|
||||
case 25:
|
||||
return "MERGE";
|
||||
case 26:
|
||||
return "BASELINE-CONTROL";
|
||||
case 27:
|
||||
return "MKACTIVITY";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
public void setProtocol(String protocol) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public void setUri(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void setRemoteAddr(String remoteAddr) {
|
||||
this.remoteAddr = remoteAddr;
|
||||
}
|
||||
|
||||
public String getRemoteAddr() {
|
||||
return remoteAddr;
|
||||
}
|
||||
|
||||
public void setRemoteHost(String remoteHost) {
|
||||
this.remoteHost = remoteHost;
|
||||
}
|
||||
|
||||
public String getRemoteHost() {
|
||||
return remoteHost;
|
||||
}
|
||||
|
||||
public void setServerName(String serverName) {
|
||||
this.serverName = serverName;
|
||||
}
|
||||
|
||||
public String getServerName() {
|
||||
return serverName;
|
||||
}
|
||||
|
||||
public void setServerPort(int serverPort) {
|
||||
this.serverPort = serverPort;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public void setSsl(boolean ssl) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
public boolean isSsl() {
|
||||
return ssl;
|
||||
}
|
||||
|
||||
public void connect() throws IOException {
|
||||
socket = SocketFactory.getDefault().createSocket(host, port);
|
||||
}
|
||||
|
||||
public void disconnect() throws IOException {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a message to request the given URL.
|
||||
*/
|
||||
public TesterAjpMessage createForwardMessage() {
|
||||
|
||||
TesterAjpMessage message = new TesterAjpMessage(packetSize);
|
||||
message.reset();
|
||||
|
||||
// Set the header bytes
|
||||
message.getBuffer()[0] = 0x12;
|
||||
message.getBuffer()[1] = 0x34;
|
||||
|
||||
// Code 2 for forward request
|
||||
message.appendByte(Constants.JK_AJP13_FORWARD_REQUEST);
|
||||
|
||||
// HTTP method, GET = 2
|
||||
message.appendByte(method);
|
||||
|
||||
// Protocol
|
||||
message.appendString(protocol);
|
||||
|
||||
// Request URI
|
||||
message.appendString(uri);
|
||||
|
||||
// Client address
|
||||
message.appendString(remoteAddr);
|
||||
|
||||
// Client host
|
||||
message.appendString(remoteHost);
|
||||
|
||||
// Server name
|
||||
message.appendString(serverName);
|
||||
|
||||
// Server port
|
||||
message.appendInt(serverPort);
|
||||
|
||||
// Is ssl
|
||||
message.appendByte(ssl ? 0x01 : 0x00);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public TesterAjpMessage createBodyMessage(byte[] data) {
|
||||
|
||||
TesterAjpMessage message = new TesterAjpMessage(packetSize);
|
||||
message.reset();
|
||||
|
||||
// Set the header bytes
|
||||
message.getBuffer()[0] = 0x12;
|
||||
message.getBuffer()[1] = 0x34;
|
||||
|
||||
message.appendBytes(data, 0, data.length);
|
||||
message.end();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sends an TesterAjpMessage to the server and returns the response message.
|
||||
*/
|
||||
public TesterAjpMessage sendMessage(TesterAjpMessage headers)
|
||||
throws IOException {
|
||||
return sendMessage(headers, null);
|
||||
}
|
||||
|
||||
public TesterAjpMessage sendMessage(TesterAjpMessage headers,
|
||||
TesterAjpMessage body) throws IOException {
|
||||
// Send the headers
|
||||
socket.getOutputStream().write(
|
||||
headers.getBuffer(), 0, headers.getLen());
|
||||
if (body != null) {
|
||||
// Send the body of present
|
||||
socket.getOutputStream().write(
|
||||
body.getBuffer(), 0, body.getLen());
|
||||
}
|
||||
// Read the response
|
||||
return readMessage();
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests the connection to the server and returns the CPONG response.
|
||||
*/
|
||||
public TesterAjpMessage cping() throws IOException {
|
||||
// Send the ping message
|
||||
socket.getOutputStream().write(AJP_CPING);
|
||||
// Read the response
|
||||
return readMessage();
|
||||
}
|
||||
|
||||
/*
|
||||
* Reads a message from the server.
|
||||
*/
|
||||
public TesterAjpMessage readMessage() throws IOException {
|
||||
|
||||
InputStream is = socket.getInputStream();
|
||||
|
||||
TesterAjpMessage message = new TesterAjpMessage(packetSize);
|
||||
|
||||
byte[] buf = message.getBuffer();
|
||||
|
||||
read(is, buf, 0, Constants.H_SIZE);
|
||||
|
||||
int messageLength = message.processHeader(false);
|
||||
if (messageLength < 0) {
|
||||
throw new IOException("Invalid AJP message length");
|
||||
} else if (messageLength == 0) {
|
||||
return message;
|
||||
} else {
|
||||
if (messageLength > buf.length) {
|
||||
throw new IllegalArgumentException("Message too long [" +
|
||||
Integer.valueOf(messageLength) +
|
||||
"] for buffer length [" +
|
||||
Integer.valueOf(buf.length) + "]");
|
||||
}
|
||||
read(is, buf, Constants.H_SIZE, messageLength);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean read(InputStream is, byte[] buf, int pos, int n)
|
||||
throws IOException {
|
||||
|
||||
int read = 0;
|
||||
int res = 0;
|
||||
while (read < n) {
|
||||
res = is.read(buf, read + pos, n - read);
|
||||
if (res > 0) {
|
||||
read += res;
|
||||
} else {
|
||||
throw new IOException("Read failed");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1032
test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java
Normal file
1032
test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java
Normal file
File diff suppressed because it is too large
Load Diff
194
test/org/apache/coyote/ajp/TesterAjpMessage.java
Normal file
194
test/org/apache/coyote/ajp/TesterAjpMessage.java
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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.ajp;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Extends {@link AjpMessage} to provide additional methods for reading from the
|
||||
* message.
|
||||
* TODO: See if it makes sense for any/all of these methods to be transferred to
|
||||
* AjpMessage
|
||||
*/
|
||||
public class TesterAjpMessage extends AjpMessage {
|
||||
|
||||
private final List<Header> headers = new ArrayList<>();
|
||||
private final List<Attribute> attributes = new ArrayList<>();
|
||||
|
||||
|
||||
public TesterAjpMessage(int packetSize) {
|
||||
super(packetSize);
|
||||
}
|
||||
|
||||
public byte readByte() {
|
||||
return buf[pos++];
|
||||
}
|
||||
|
||||
public int readInt() {
|
||||
int val = (buf[pos++] & 0xFF ) << 8;
|
||||
val += buf[pos++] & 0xFF;
|
||||
return val;
|
||||
}
|
||||
|
||||
public String readString() {
|
||||
int len = readInt();
|
||||
return readString(len);
|
||||
}
|
||||
|
||||
public String readString(int len) {
|
||||
StringBuilder buffer = new StringBuilder(len);
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = (char) buf[pos++];
|
||||
buffer.append(c);
|
||||
}
|
||||
// Read end of string marker
|
||||
readByte();
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
public String readHeaderName() {
|
||||
byte b = readByte();
|
||||
if ((b & 0xFF) == 0xA0) {
|
||||
// Coded header
|
||||
return Constants.getResponseHeaderForCode(readByte());
|
||||
} else {
|
||||
int len = (b & 0xFF) << 8;
|
||||
len += getByte() & 0xFF;
|
||||
return readString(len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addHeader(int code, String value) {
|
||||
headers.add(new Header(code, value));
|
||||
}
|
||||
|
||||
|
||||
public void addHeader(String name, String value) {
|
||||
headers.add(new Header(name, value));
|
||||
}
|
||||
|
||||
|
||||
public void addAttribute(int code, String value) {
|
||||
attributes.add(new Attribute(code, value));
|
||||
}
|
||||
|
||||
|
||||
public void addAttribute(String name, String value) {
|
||||
attributes.add(new Attribute(name, value));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
// Add the header count
|
||||
appendInt(headers.size());
|
||||
|
||||
for (Header header : headers) {
|
||||
header.append(this);
|
||||
}
|
||||
|
||||
for (Attribute attribute : attributes) {
|
||||
attribute.append(this);
|
||||
}
|
||||
|
||||
// Terminator
|
||||
appendByte(0xFF);
|
||||
|
||||
len = pos;
|
||||
int dLen = len - 4;
|
||||
|
||||
buf[0] = (byte) 0x12;
|
||||
buf[1] = (byte) 0x34;
|
||||
buf[2] = (byte) ((dLen>>>8) & 0xFF);
|
||||
buf[3] = (byte) (dLen & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
headers.clear();
|
||||
}
|
||||
|
||||
|
||||
public void appendString(String string) {
|
||||
byte[] bytes = string.getBytes(StandardCharsets.ISO_8859_1);
|
||||
appendBytes(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
|
||||
private static class Header {
|
||||
private final int code;
|
||||
private final String name;
|
||||
private final String value;
|
||||
|
||||
public Header(int code, String value) {
|
||||
this.code = code;
|
||||
this.name = null;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Header(String name, String value) {
|
||||
this.code = 0;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void append(TesterAjpMessage message) {
|
||||
if (code == 0) {
|
||||
message.appendString(name);
|
||||
} else {
|
||||
message.appendInt(code);
|
||||
}
|
||||
message.appendString(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class Attribute {
|
||||
private final int code;
|
||||
private final String name;
|
||||
private final String value;
|
||||
|
||||
public Attribute(int code, String value) {
|
||||
this.code = code;
|
||||
this.name = null;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Attribute(String name, String value) {
|
||||
this.code = 0;
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void append(TesterAjpMessage message) {
|
||||
if (code == 0) {
|
||||
message.appendByte(0x0A);
|
||||
message.appendString(name);
|
||||
} else {
|
||||
message.appendByte(code);
|
||||
}
|
||||
message.appendString(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.http11;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class TestAbstractHttp11Protocol {
|
||||
|
||||
@Test
|
||||
public void testGetSslProtocol() {
|
||||
Http11Nio2Protocol protocol = new Http11Nio2Protocol();
|
||||
protocol.getSSLProtocol();
|
||||
}
|
||||
}
|
||||
691
test/org/apache/coyote/http11/TestHttp11InputBuffer.java
Normal file
691
test/org/apache/coyote/http11/TestHttp11InputBuffer.java
Normal file
@@ -0,0 +1,691 @@
|
||||
/*
|
||||
* 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.http11;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Enumeration;
|
||||
|
||||
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.connector.Connector;
|
||||
import org.apache.catalina.startup.SimpleHttpClient;
|
||||
import org.apache.catalina.startup.TesterServlet;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
|
||||
public class TestHttp11InputBuffer extends TomcatBaseTest {
|
||||
|
||||
/**
|
||||
* Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=48839
|
||||
*/
|
||||
@Test
|
||||
public void testBug48839() {
|
||||
|
||||
Bug48839Client client = new Bug48839Client();
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bug 48839 test client.
|
||||
*/
|
||||
private class Bug48839Client extends SimpleHttpClient {
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Tomcat.addServlet(root, "Bug48839", new Bug48839Servlet());
|
||||
root.addServletMappingDecoded("/test", "Bug48839");
|
||||
|
||||
try {
|
||||
tomcat.start();
|
||||
setPort(tomcat.getConnector().getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[1];
|
||||
request[0] =
|
||||
"GET http://localhost:8080/test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"X-Bug48839: abcd" + CRLF +
|
||||
"\tefgh" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("abcd\tefgh")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Bug48839Servlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Only interested in the request headers from a GET request
|
||||
*/
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
// Just echo the header value back as plain text
|
||||
resp.setContentType("text/plain");
|
||||
|
||||
PrintWriter out = resp.getWriter();
|
||||
|
||||
Enumeration<String> values = req.getHeaders("X-Bug48839");
|
||||
while (values.hasMoreElements()) {
|
||||
out.println(values.nextElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557Valid() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("X-Bug51557Valid", "1234");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("1234abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557Invalid() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("X-Bug51557=Invalid", "1234", true);
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse400());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557NoColon() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("X-Bug51557NoColon");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557SeparatorsInName() throws Exception {
|
||||
char httpSeparators[] = new char[] {
|
||||
'\t', ' ', '\"', '(', ')', ',', '/', ':', ';', '<',
|
||||
'=', '>', '?', '@', '[', '\\', ']', '{', '}' };
|
||||
|
||||
for (char s : httpSeparators) {
|
||||
doTestBug51557CharInName(s);
|
||||
tearDown();
|
||||
setUp();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557CtlInName() throws Exception {
|
||||
for (int i = 0; i < 31; i++) {
|
||||
doTestBug51557CharInName((char) i);
|
||||
tearDown();
|
||||
setUp();
|
||||
}
|
||||
doTestBug51557CharInName((char) 127);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557CtlInValue() throws Exception {
|
||||
for (int i = 0; i < 31; i++) {
|
||||
if (i == '\t') {
|
||||
// TAB is allowed
|
||||
continue;
|
||||
}
|
||||
doTestBug51557InvalidCharInValue((char) i);
|
||||
tearDown();
|
||||
setUp();
|
||||
}
|
||||
doTestBug51557InvalidCharInValue((char) 127);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557ObsTextInValue() throws Exception {
|
||||
for (int i = 128; i < 255; i++) {
|
||||
doTestBug51557ValidCharInValue((char) i);
|
||||
tearDown();
|
||||
setUp();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557Continuation() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("X-Bug=51557NoColon",
|
||||
"foo" + SimpleHttpClient.CRLF + " bar");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557BoundaryStart() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("=X-Bug51557",
|
||||
"invalid");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBug51557BoundaryEnd() {
|
||||
|
||||
Bug51557Client client = new Bug51557Client("X-Bug51557=",
|
||||
"invalid");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
private void doTestBug51557CharInName(char s) {
|
||||
Bug51557Client client =
|
||||
new Bug51557Client("X-Bug" + s + "51557", "invalid");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
private void doTestBug51557InvalidCharInValue(char s) {
|
||||
Bug51557Client client =
|
||||
new Bug51557Client("X-Bug51557-Invalid", "invalid" + s + "invalid");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue("Testing [" + (int) s + "]", client.isResponse200());
|
||||
Assert.assertEquals("Testing [" + (int) s + "]", "abcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
private void doTestBug51557ValidCharInValue(char s) {
|
||||
Bug51557Client client =
|
||||
new Bug51557Client("X-Bug51557-Valid", "valid" + s + "valid");
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue("Testing [" + (int) s + "]", client.isResponse200());
|
||||
Assert.assertEquals("Testing [" + (int) s + "]", "valid" + s + "validabcd", client.getResponseBody());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bug 51557 test client.
|
||||
*/
|
||||
private class Bug51557Client extends SimpleHttpClient {
|
||||
|
||||
private final String headerName;
|
||||
private final String headerLine;
|
||||
private final boolean rejectIllegalHeader;
|
||||
|
||||
public Bug51557Client(String headerName) {
|
||||
this.headerName = headerName;
|
||||
this.headerLine = headerName;
|
||||
this.rejectIllegalHeader = false;
|
||||
}
|
||||
|
||||
public Bug51557Client(String headerName, String headerValue) {
|
||||
this(headerName, headerValue, false);
|
||||
}
|
||||
|
||||
public Bug51557Client(String headerName, String headerValue,
|
||||
boolean rejectIllegalHeader) {
|
||||
this.headerName = headerName;
|
||||
this.headerLine = headerName + ": " + headerValue;
|
||||
this.rejectIllegalHeader = rejectIllegalHeader;
|
||||
}
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Tomcat.addServlet(root, "Bug51557",
|
||||
new Bug51557Servlet(headerName));
|
||||
root.addServletMappingDecoded("/test", "Bug51557");
|
||||
|
||||
try {
|
||||
Connector connector = tomcat.getConnector();
|
||||
Assert.assertTrue(connector.setProperty(
|
||||
"rejectIllegalHeader", Boolean.toString(rejectIllegalHeader)));
|
||||
tomcat.start();
|
||||
setPort(connector.getLocalPort());
|
||||
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[1];
|
||||
request[0] =
|
||||
"GET http://localhost:8080/test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
headerLine + CRLF +
|
||||
"X-Bug51557: abcd" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("abcd")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Bug51557Servlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String invalidHeaderName;
|
||||
|
||||
/**
|
||||
* @param invalidHeaderName The header name should be invalid and
|
||||
* therefore ignored by the header parsing code
|
||||
*/
|
||||
public Bug51557Servlet(String invalidHeaderName) {
|
||||
this.invalidHeaderName = invalidHeaderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only interested in the request headers from a GET request
|
||||
*/
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
// Just echo the header value back as plain text
|
||||
resp.setContentType("text/plain");
|
||||
|
||||
PrintWriter out = resp.getWriter();
|
||||
|
||||
processHeaders(invalidHeaderName, req, out);
|
||||
processHeaders("X-Bug51557", req, out);
|
||||
}
|
||||
|
||||
private void processHeaders(String header, HttpServletRequest req,
|
||||
PrintWriter out) {
|
||||
Enumeration<String> values = req.getHeaders(header);
|
||||
while (values.hasMoreElements()) {
|
||||
out.println(values.nextElement());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test case for new lines at the start of a request. RFC2616
|
||||
* does not permit any, but Tomcat is tolerant of them if they are present.
|
||||
*/
|
||||
@Test
|
||||
public void testNewLines() {
|
||||
|
||||
NewLinesClient client = new NewLinesClient(10);
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test case for new lines at the start of a request. RFC2616
|
||||
* does not permit any, but Tomcat is tolerant of them if they are present.
|
||||
*/
|
||||
@Test
|
||||
public void testNewLinesExcessive() {
|
||||
|
||||
NewLinesClient client = new NewLinesClient(10000);
|
||||
|
||||
// If the connection is closed fast enough, writing the request will
|
||||
// fail and the response won't be read.
|
||||
Exception e = client.doRequest();
|
||||
if (e == null) {
|
||||
Assert.assertTrue(client.getResponseLine(), client.isResponse400());
|
||||
}
|
||||
Assert.assertFalse(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
private class NewLinesClient extends SimpleHttpClient {
|
||||
|
||||
private final String newLines;
|
||||
|
||||
private NewLinesClient(int count) {
|
||||
StringBuilder sb = new StringBuilder(count * 2);
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(CRLF);
|
||||
}
|
||||
newLines = sb.toString();
|
||||
}
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Tomcat.addServlet(root, "test", new TesterServlet());
|
||||
root.addServletMappingDecoded("/test", "test");
|
||||
|
||||
try {
|
||||
tomcat.start();
|
||||
setPort(tomcat.getConnector().getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[1];
|
||||
request[0] =
|
||||
newLines +
|
||||
"GET http://localhost:8080/test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"X-Bug48839: abcd" + CRLF +
|
||||
"\tefgh" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("OK")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=54947
|
||||
*/
|
||||
@Test
|
||||
public void testBug54947() {
|
||||
|
||||
Bug54947Client client = new Bug54947Client();
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bug 54947 test client.
|
||||
*/
|
||||
private class Bug54947Client extends SimpleHttpClient {
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Tomcat.addServlet(root, "Bug54947", new TesterServlet());
|
||||
root.addServletMappingDecoded("/test", "Bug54947");
|
||||
|
||||
try {
|
||||
tomcat.start();
|
||||
setPort(tomcat.getConnector().getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[2];
|
||||
request[0] = "GET http://localhost:8080/test HTTP/1.1" + CR;
|
||||
request[1] = LF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("OK")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=59089
|
||||
*/
|
||||
@Test
|
||||
public void testBug59089() {
|
||||
|
||||
Bug59089Client client = new Bug59089Client();
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bug 59089 test client.
|
||||
*/
|
||||
private class Bug59089Client extends SimpleHttpClient {
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
// Ensure body is read correctly
|
||||
setUseContentLength(true);
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Context root = tomcat.addContext("", TEMP_DIR);
|
||||
Tomcat.addServlet(root, "Bug59089", new TesterServlet());
|
||||
root.addServletMappingDecoded("/test", "Bug59089");
|
||||
|
||||
try {
|
||||
Connector connector = tomcat.getConnector();
|
||||
Assert.assertTrue(connector.setProperty("rejectIllegalHeader", "false"));
|
||||
tomcat.start();
|
||||
setPort(connector.getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[1];
|
||||
request[0] = "GET http://localhost:8080/test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"X-Header: Ignore" + CRLF +
|
||||
"X-Header" + (char) 130 + ": Broken" + CRLF + CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!getResponseBody().contains("OK")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testInvalidMethod() {
|
||||
|
||||
InvalidMethodClient client = new InvalidMethodClient();
|
||||
|
||||
client.doRequest();
|
||||
Assert.assertTrue(client.getResponseLine(), client.isResponse400());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bug 48839 test client.
|
||||
*/
|
||||
private class InvalidMethodClient extends SimpleHttpClient {
|
||||
|
||||
private Exception doRequest() {
|
||||
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
tomcat.addContext("", TEMP_DIR);
|
||||
|
||||
try {
|
||||
tomcat.start();
|
||||
setPort(tomcat.getConnector().getLocalPort());
|
||||
|
||||
// Open connection
|
||||
connect();
|
||||
|
||||
String[] request = new String[1];
|
||||
request[0] =
|
||||
"GET" + (char) 0 + " /test HTTP/1.1" + CRLF +
|
||||
"Host: localhost:8080" + CRLF +
|
||||
"Connection: close" + CRLF +
|
||||
CRLF;
|
||||
|
||||
setRequest(request);
|
||||
processRequest(); // blocks until response has been read
|
||||
|
||||
// Close the connection
|
||||
disconnect();
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
if (getResponseBody() == null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
89
test/org/apache/coyote/http11/TestHttp11OutputBuffer.java
Normal file
89
test/org/apache/coyote/http11/TestHttp11OutputBuffer.java
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.http11;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.startup.SimpleHttpClient;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
|
||||
public class TestHttp11OutputBuffer extends TomcatBaseTest {
|
||||
|
||||
@Test
|
||||
public void testSendAck() throws Exception {
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
Tomcat.addServlet(ctx, "echo", new EchoBodyServlet());
|
||||
ctx.addServletMappingDecoded("/echo", "echo");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
ExpectationClient client = new ExpectationClient();
|
||||
|
||||
client.setPort(tomcat.getConnector().getLocalPort());
|
||||
// Expected content doesn't end with a CR-LF so if it isn't chunked make
|
||||
// sure the content length is used as reading it line-by-line will fail
|
||||
// since there is no "line".
|
||||
client.setUseContentLength(true);
|
||||
|
||||
client.connect();
|
||||
|
||||
client.doRequestHeaders();
|
||||
Assert.assertTrue(client.isResponse100());
|
||||
|
||||
client.doRequestBody();
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertTrue(client.isResponseBodyOK());
|
||||
}
|
||||
|
||||
private static class ExpectationClient extends SimpleHttpClient {
|
||||
|
||||
private static final String BODY = "foo=bar";
|
||||
|
||||
public void doRequestHeaders() throws Exception {
|
||||
StringBuilder requestHeaders = new StringBuilder();
|
||||
requestHeaders.append("POST /echo HTTP/1.1").append(CRLF);
|
||||
requestHeaders.append("Host: localhost").append(CRLF);
|
||||
requestHeaders.append("Expect: 100-continue").append(CRLF);
|
||||
requestHeaders.append("Content-Type: application/x-www-form-urlencoded").append(CRLF);
|
||||
String len = Integer.toString(BODY.length());
|
||||
requestHeaders.append("Content-length: ").append(len).append(CRLF);
|
||||
requestHeaders.append(CRLF);
|
||||
|
||||
setRequest(new String[] {requestHeaders.toString()});
|
||||
|
||||
processRequest(false);
|
||||
}
|
||||
|
||||
public void doRequestBody() throws Exception {
|
||||
setRequest(new String[] { BODY });
|
||||
|
||||
processRequest(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
return BODY.equals(getResponseBody());
|
||||
}
|
||||
}
|
||||
}
|
||||
1662
test/org/apache/coyote/http11/TestHttp11Processor.java
Normal file
1662
test/org/apache/coyote/http11/TestHttp11Processor.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* 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.http11.filters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
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.SimpleHttpClient;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
|
||||
public class TestChunkedInputFilter extends TomcatBaseTest {
|
||||
|
||||
private static final String LF = "\n";
|
||||
private static final int EXT_SIZE_LIMIT = 10;
|
||||
|
||||
@Test
|
||||
public void testChunkHeaderCRLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkHeaderLF() throws Exception {
|
||||
doTestChunkingCRLF(false, true, true, true, true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkCRLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkLF() throws Exception {
|
||||
doTestChunkingCRLF(true, false, true, true, true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstTrailingHeadersCRLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstTrailingHeadersLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, false, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondTrailingHeadersCRLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondTrailingHeadersLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, false, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndCRLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndLF() throws Exception {
|
||||
doTestChunkingCRLF(true, true, true, true, false, false);
|
||||
}
|
||||
|
||||
private void doTestChunkingCRLF(boolean chunkHeaderUsesCRLF,
|
||||
boolean chunkUsesCRLF, boolean firstheaderUsesCRLF,
|
||||
boolean secondheaderUsesCRLF, boolean endUsesCRLF,
|
||||
boolean expectPass) throws Exception {
|
||||
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
// Configure allowed trailer headers
|
||||
Assert.assertTrue(tomcat.getConnector().setProperty("allowedTrailerHeaders", "x-trailer1,x-trailer2"));
|
||||
|
||||
EchoHeaderServlet servlet = new EchoHeaderServlet(expectPass);
|
||||
Tomcat.addServlet(ctx, "servlet", servlet);
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
String[] request = new String[]{
|
||||
"POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF +
|
||||
"Host: any" + SimpleHttpClient.CRLF +
|
||||
"Transfer-encoding: chunked" + SimpleHttpClient.CRLF +
|
||||
"Content-Type: application/x-www-form-urlencoded" +
|
||||
SimpleHttpClient.CRLF +
|
||||
"Connection: close" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF +
|
||||
"3" + (chunkHeaderUsesCRLF ? SimpleHttpClient.CRLF : LF) +
|
||||
"a=0" + (chunkUsesCRLF ? SimpleHttpClient.CRLF : LF) +
|
||||
"4" + SimpleHttpClient.CRLF +
|
||||
"&b=1" + SimpleHttpClient.CRLF +
|
||||
"0" + SimpleHttpClient.CRLF +
|
||||
"x-trailer1: Test", "Value1" +
|
||||
(firstheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) +
|
||||
"x-trailer2: TestValue2" +
|
||||
(secondheaderUsesCRLF ? SimpleHttpClient.CRLF : LF) +
|
||||
(endUsesCRLF ? SimpleHttpClient.CRLF : LF) };
|
||||
|
||||
TrailerClient client =
|
||||
new TrailerClient(tomcat.getConnector().getLocalPort());
|
||||
client.setRequest(request);
|
||||
|
||||
client.connect();
|
||||
Exception processException = null;
|
||||
try {
|
||||
client.processRequest();
|
||||
} catch (Exception e) {
|
||||
// Socket was probably closed before client had a chance to read
|
||||
// response
|
||||
processException = e;
|
||||
}
|
||||
|
||||
if (expectPass) {
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
Assert.assertEquals("nullnull7TestValue1TestValue2",
|
||||
client.getResponseBody());
|
||||
Assert.assertNull(processException);
|
||||
Assert.assertFalse(servlet.getExceptionDuringRead());
|
||||
} else {
|
||||
if (processException == null) {
|
||||
Assert.assertTrue(client.getResponseLine(), client.isResponse500());
|
||||
} else {
|
||||
// Use fall-back for checking the error occurred
|
||||
Assert.assertTrue(servlet.getExceptionDuringRead());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrailingHeadersSizeLimit() throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(false));
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
// Limit the size of the trailing header
|
||||
Assert.assertTrue(tomcat.getConnector().setProperty("maxTrailerSize", "10"));
|
||||
tomcat.start();
|
||||
|
||||
String[] request = new String[]{
|
||||
"POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF +
|
||||
"Host: any" + SimpleHttpClient.CRLF +
|
||||
"Transfer-encoding: chunked" + SimpleHttpClient.CRLF +
|
||||
"Content-Type: application/x-www-form-urlencoded" +
|
||||
SimpleHttpClient.CRLF +
|
||||
"Connection: close" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF +
|
||||
"3" + SimpleHttpClient.CRLF +
|
||||
"a=0" + SimpleHttpClient.CRLF +
|
||||
"4" + SimpleHttpClient.CRLF +
|
||||
"&b=1" + SimpleHttpClient.CRLF +
|
||||
"0" + SimpleHttpClient.CRLF +
|
||||
"x-trailer: Test" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF };
|
||||
|
||||
TrailerClient client =
|
||||
new TrailerClient(tomcat.getConnector().getLocalPort());
|
||||
client.setRequest(request);
|
||||
|
||||
client.connect();
|
||||
client.processRequest();
|
||||
// Expected to fail because the trailers are longer
|
||||
// than the set limit of 10 bytes
|
||||
Assert.assertTrue(client.isResponse500());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExtensionSizeLimitOneBelow() throws Exception {
|
||||
doTestExtensionSizeLimit(EXT_SIZE_LIMIT - 1, true);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExtensionSizeLimitExact() throws Exception {
|
||||
doTestExtensionSizeLimit(EXT_SIZE_LIMIT, true);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExtensionSizeLimitOneOver() throws Exception {
|
||||
doTestExtensionSizeLimit(EXT_SIZE_LIMIT + 1, false);
|
||||
}
|
||||
|
||||
|
||||
private void doTestExtensionSizeLimit(int len, boolean ok) throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
Assert.assertTrue(tomcat.getConnector().setProperty(
|
||||
"maxExtensionSize", Integer.toString(EXT_SIZE_LIMIT)));
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(ok));
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
String extName = ";foo=";
|
||||
StringBuilder extValue = new StringBuilder(len);
|
||||
for (int i = 0; i < (len - extName.length()); i++) {
|
||||
extValue.append("x");
|
||||
}
|
||||
|
||||
String[] request = new String[]{
|
||||
"POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF +
|
||||
"Host: any" + SimpleHttpClient.CRLF +
|
||||
"Transfer-encoding: chunked" + SimpleHttpClient.CRLF +
|
||||
"Content-Type: application/x-www-form-urlencoded" +
|
||||
SimpleHttpClient.CRLF +
|
||||
"Connection: close" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF +
|
||||
"3" + extName + extValue.toString() + SimpleHttpClient.CRLF +
|
||||
"a=0" + SimpleHttpClient.CRLF +
|
||||
"4" + SimpleHttpClient.CRLF +
|
||||
"&b=1" + SimpleHttpClient.CRLF +
|
||||
"0" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF };
|
||||
|
||||
TrailerClient client =
|
||||
new TrailerClient(tomcat.getConnector().getLocalPort());
|
||||
client.setRequest(request);
|
||||
|
||||
client.connect();
|
||||
client.processRequest();
|
||||
|
||||
if (ok) {
|
||||
Assert.assertTrue(client.isResponse200());
|
||||
} else {
|
||||
Assert.assertTrue(client.isResponse500());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoTrailingHeaders() throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(true));
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
String request =
|
||||
"POST /echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF +
|
||||
"Host: any" + SimpleHttpClient.CRLF +
|
||||
"Transfer-encoding: chunked" + SimpleHttpClient.CRLF +
|
||||
"Content-Type: application/x-www-form-urlencoded" +
|
||||
SimpleHttpClient.CRLF +
|
||||
"Connection: close" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF +
|
||||
"3" + SimpleHttpClient.CRLF +
|
||||
"a=0" + SimpleHttpClient.CRLF +
|
||||
"4" + SimpleHttpClient.CRLF +
|
||||
"&b=1" + SimpleHttpClient.CRLF +
|
||||
"0" + SimpleHttpClient.CRLF +
|
||||
SimpleHttpClient.CRLF;
|
||||
|
||||
TrailerClient client =
|
||||
new TrailerClient(tomcat.getConnector().getLocalPort());
|
||||
client.setRequest(new String[] {request});
|
||||
|
||||
client.connect();
|
||||
client.processRequest();
|
||||
Assert.assertEquals("nullnull7nullnull", client.getResponseBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeZero() throws Exception {
|
||||
doTestChunkSize(true, true, "", 10, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeAbsent() throws Exception {
|
||||
doTestChunkSize(false, false, SimpleHttpClient.CRLF, 10, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeTwentyFive() throws Exception {
|
||||
doTestChunkSize(true, true, "19" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!Hello World!!" + SimpleHttpClient.CRLF, 40, 25);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeEightDigit() throws Exception {
|
||||
doTestChunkSize(true, true, "0000000C" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!" + SimpleHttpClient.CRLF, 20, 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeNineDigit() throws Exception {
|
||||
doTestChunkSize(false, false, "00000000C" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!" + SimpleHttpClient.CRLF, 20, 12);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeLong() throws Exception {
|
||||
doTestChunkSize(true, false, "7fFFffFF" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!" + SimpleHttpClient.CRLF, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeIntegerMinValue() throws Exception {
|
||||
doTestChunkSize(false, false, "80000000" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!" + SimpleHttpClient.CRLF, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChunkSizeMinusOne() throws Exception {
|
||||
doTestChunkSize(false, false, "ffffffff" + SimpleHttpClient.CRLF
|
||||
+ "Hello World!" + SimpleHttpClient.CRLF, 10, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expectPass
|
||||
* If the servlet is expected to process the request
|
||||
* @param expectReadWholeBody
|
||||
* If the servlet is expected to fully read the body and reliably
|
||||
* deliver a response
|
||||
* @param chunks
|
||||
* Text of chunks
|
||||
* @param readLimit
|
||||
* Do not read more than this many bytes
|
||||
* @param expectReadCount
|
||||
* Expected count of read bytes
|
||||
* @throws Exception
|
||||
* Unexpected
|
||||
*/
|
||||
private void doTestChunkSize(boolean expectPass,
|
||||
boolean expectReadWholeBody, String chunks, int readLimit,
|
||||
int expectReadCount) throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
BodyReadServlet servlet = new BodyReadServlet(expectPass, readLimit);
|
||||
Tomcat.addServlet(ctx, "servlet", servlet);
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
String request = "POST /echo-params.jsp HTTP/1.1"
|
||||
+ SimpleHttpClient.CRLF + "Host: any" + SimpleHttpClient.CRLF
|
||||
+ "Transfer-encoding: chunked" + SimpleHttpClient.CRLF
|
||||
+ "Content-Type: text/plain" + SimpleHttpClient.CRLF;
|
||||
if (expectPass) {
|
||||
request += "Connection: close" + SimpleHttpClient.CRLF;
|
||||
}
|
||||
request += SimpleHttpClient.CRLF + chunks + "0" + SimpleHttpClient.CRLF
|
||||
+ SimpleHttpClient.CRLF;
|
||||
|
||||
TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort());
|
||||
// Need to use the content length here as variations in Connector and
|
||||
// JVM+OS behaviour mean that in some circumstances the client may see
|
||||
// an IOException rather than the response body when the server closes
|
||||
// the connection.
|
||||
client.setUseContentLength(true);
|
||||
client.setRequest(new String[] { request });
|
||||
|
||||
Exception processException = null;
|
||||
client.connect();
|
||||
try {
|
||||
client.processRequest();
|
||||
client.disconnect();
|
||||
} catch (Exception e) {
|
||||
// Socket was probably closed before client had a chance to read
|
||||
// response
|
||||
processException = e;
|
||||
}
|
||||
if (expectPass) {
|
||||
if (expectReadWholeBody) {
|
||||
Assert.assertNull(processException);
|
||||
}
|
||||
if (processException == null) {
|
||||
Assert.assertTrue(client.getResponseLine(), client.isResponse200());
|
||||
Assert.assertEquals(String.valueOf(expectReadCount),
|
||||
client.getResponseBody());
|
||||
}
|
||||
Assert.assertEquals(expectReadCount, servlet.getCountRead());
|
||||
} else {
|
||||
if (processException == null) {
|
||||
Assert.assertTrue(client.getResponseLine(), client.isResponse500());
|
||||
}
|
||||
Assert.assertEquals(0, servlet.getCountRead());
|
||||
Assert.assertTrue(servlet.getExceptionDuringRead());
|
||||
}
|
||||
}
|
||||
|
||||
private static class EchoHeaderServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean exceptionDuringRead = false;
|
||||
|
||||
private final boolean expectPass;
|
||||
|
||||
public EchoHeaderServlet(boolean expectPass) {
|
||||
this.expectPass = expectPass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
resp.setContentType("text/plain");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
// Headers not visible yet, body not processed
|
||||
dumpHeader("x-trailer1", req, pw);
|
||||
dumpHeader("x-trailer2", req, pw);
|
||||
|
||||
// Read the body - quick and dirty
|
||||
InputStream is = req.getInputStream();
|
||||
int count = 0;
|
||||
try {
|
||||
while (is.read() > -1) {
|
||||
count++;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
exceptionDuringRead = true;
|
||||
if (!expectPass) { // as expected
|
||||
log(ioe.toString());
|
||||
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
pw.write(Integer.toString(count));
|
||||
|
||||
// Headers should be visible now
|
||||
dumpHeader("x-trailer1", req, pw);
|
||||
dumpHeader("x-trailer2", req, pw);
|
||||
}
|
||||
|
||||
public boolean getExceptionDuringRead() {
|
||||
return exceptionDuringRead;
|
||||
}
|
||||
|
||||
private void dumpHeader(String headerName, HttpServletRequest req,
|
||||
PrintWriter pw) {
|
||||
String value = req.getHeader(headerName);
|
||||
if (value == null) {
|
||||
value = "null";
|
||||
}
|
||||
pw.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BodyReadServlet extends HttpServlet {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean exceptionDuringRead = false;
|
||||
private int countRead = 0;
|
||||
private final boolean expectPass;
|
||||
private final int readLimit;
|
||||
|
||||
public BodyReadServlet(boolean expectPass, int readLimit) {
|
||||
this.expectPass = expectPass;
|
||||
this.readLimit = readLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
resp.setContentType("text/plain");
|
||||
PrintWriter pw = resp.getWriter();
|
||||
|
||||
// Read the body - quick and dirty
|
||||
InputStream is = req.getInputStream();
|
||||
try {
|
||||
while (is.read() > -1 && countRead < readLimit) {
|
||||
countRead++;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
exceptionDuringRead = true;
|
||||
if (!expectPass) { // as expected
|
||||
log(ioe.toString());
|
||||
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
pw.write(Integer.toString(countRead));
|
||||
}
|
||||
|
||||
public boolean getExceptionDuringRead() {
|
||||
return exceptionDuringRead;
|
||||
}
|
||||
|
||||
public int getCountRead() {
|
||||
return countRead;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrailerClient extends SimpleHttpClient {
|
||||
|
||||
public TrailerClient(int port) {
|
||||
setPort(port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResponseBodyOK() {
|
||||
return getResponseBody().contains("TestTestTest");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.http11.filters;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.coyote.Response;
|
||||
|
||||
/**
|
||||
* Test case to demonstrate the interaction between gzip and flushing in the
|
||||
* output filter.
|
||||
*/
|
||||
public class TestGzipOutputFilter {
|
||||
|
||||
/*
|
||||
* Test the interaction between gzip and flushing. The idea is to: 1. create
|
||||
* a internal output buffer, response, and attach an active gzipoutputfilter
|
||||
* to the output buffer 2. set the output stream of the internal buffer to
|
||||
* be a ByteArrayOutputStream so we can inspect the output bytes 3. write a
|
||||
* chunk out using the gzipoutputfilter and invoke a flush on the
|
||||
* InternalOutputBuffer 4. read from the ByteArrayOutputStream to find out
|
||||
* what's being written out (flushed) 5. find out what's expected by writing
|
||||
* to GZIPOutputStream and close it (to force flushing) 6. Compare the size
|
||||
* of the two arrays, they should be close (instead of one being much
|
||||
* shorter than the other one)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void testFlushingWithGzip() throws Exception {
|
||||
// set up response, InternalOutputBuffer, and ByteArrayOutputStream
|
||||
Response res = new Response();
|
||||
TesterOutputBuffer tob = new TesterOutputBuffer(res, 8 * 1024);
|
||||
res.setOutputBuffer(tob);
|
||||
|
||||
// set up GzipOutputFilter to attach to the TesterOutputBuffer
|
||||
GzipOutputFilter gf = new GzipOutputFilter();
|
||||
tob.addFilter(gf);
|
||||
tob.addActiveFilter(gf);
|
||||
|
||||
// write a chunk out
|
||||
byte[] d = "Hello there tomcat developers, there is a bug in JDK".getBytes();
|
||||
ByteBuffer bb = ByteBuffer.wrap(d);
|
||||
tob.doWrite(bb);
|
||||
|
||||
// flush the InternalOutputBuffer
|
||||
tob.flush();
|
||||
|
||||
// read from the ByteArrayOutputStream to find out what's being written
|
||||
// out (flushed)
|
||||
byte[] dataFound = tob.toByteArray();
|
||||
|
||||
// find out what's expected by writing to GZIPOutputStream and close it
|
||||
// (to force flushing)
|
||||
ByteArrayOutputStream gbos = new ByteArrayOutputStream(1024);
|
||||
GZIPOutputStream gos = new GZIPOutputStream(gbos);
|
||||
gos.write(d);
|
||||
gos.close();
|
||||
|
||||
// read the expected data
|
||||
byte[] dataExpected = gbos.toByteArray();
|
||||
|
||||
// most of the data should have been flushed out
|
||||
Assert.assertTrue(dataFound.length >= (dataExpected.length - 20));
|
||||
}
|
||||
}
|
||||
137
test/org/apache/coyote/http11/filters/TesterOutputBuffer.java
Normal file
137
test/org/apache/coyote/http11/filters/TesterOutputBuffer.java
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.http11.filters;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.apache.coyote.Response;
|
||||
import org.apache.coyote.http11.Http11OutputBuffer;
|
||||
import org.apache.coyote.http11.HttpOutputBuffer;
|
||||
import org.apache.tomcat.util.buf.ByteChunk;
|
||||
import org.apache.tomcat.util.net.SocketWrapperBase;
|
||||
|
||||
/**
|
||||
* Output buffer for use in unit tests. This is a minimal implementation.
|
||||
*/
|
||||
public class TesterOutputBuffer extends Http11OutputBuffer {
|
||||
|
||||
/**
|
||||
* Underlying output stream.
|
||||
*/
|
||||
private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
|
||||
public TesterOutputBuffer(Response response, int headerBufferSize) {
|
||||
super(response, headerBufferSize, false);
|
||||
outputStreamOutputBuffer = new OutputStreamOutputBuffer();
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------- Public Methods
|
||||
|
||||
@Override
|
||||
public void init(SocketWrapperBase<?> socketWrapper) {
|
||||
// NO-OP: Unused
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recycle the output buffer. This should be called when closing the
|
||||
* connection.
|
||||
*/
|
||||
@Override
|
||||
public void recycle() {
|
||||
super.recycle();
|
||||
outputStream = null;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------ HTTP/1.1 Output Methods
|
||||
|
||||
/**
|
||||
* Send an acknowledgement.
|
||||
*/
|
||||
@Override
|
||||
public void sendAck() {
|
||||
// NO-OP: Unused
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void commit() {
|
||||
// NO-OP: Unused
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean flushBuffer(boolean block) throws IOException {
|
||||
// Blocking IO so ignore block parameter as this will always use
|
||||
// blocking IO.
|
||||
// Always blocks so never any data left over.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Expose data written for use by unit tests.
|
||||
*/
|
||||
byte[] toByteArray() {
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This class is an output buffer which will write data to an output
|
||||
* stream.
|
||||
*/
|
||||
protected class OutputStreamOutputBuffer implements HttpOutputBuffer {
|
||||
|
||||
@Override
|
||||
public int doWrite(ByteChunk chunk) throws IOException {
|
||||
int length = chunk.getLength();
|
||||
outputStream.write(chunk.getBuffer(), chunk.getStart(), length);
|
||||
byteCount += chunk.getLength();
|
||||
return chunk.getLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int doWrite(ByteBuffer chunk) throws IOException {
|
||||
int length = chunk.remaining();
|
||||
outputStream.write(chunk.array(), chunk.arrayOffset() + chunk.position(), length);
|
||||
byteCount += length;
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBytesWritten() {
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
// NO-OP: Unused
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() throws IOException {
|
||||
// NO-OP: Unused
|
||||
}
|
||||
}
|
||||
}
|
||||
519
test/org/apache/coyote/http11/upgrade/TestUpgrade.java
Normal file
519
test/org/apache/coyote/http11/upgrade/TestUpgrade.java
Normal file
@@ -0,0 +1,519 @@
|
||||
/*
|
||||
* 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.http11.upgrade;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpUpgradeHandler;
|
||||
import javax.servlet.http.WebConnection;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
|
||||
public class TestUpgrade extends TomcatBaseTest {
|
||||
|
||||
private static final String MESSAGE = "This is a test.";
|
||||
|
||||
@Test
|
||||
public void testSimpleUpgradeBlocking() throws Exception {
|
||||
UpgradeConnection uc = doUpgrade(EchoBlocking.class);
|
||||
uc.shutdownInput();
|
||||
uc.shutdownOutput();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleUpgradeNonBlocking() throws Exception {
|
||||
UpgradeConnection uc = doUpgrade(EchoNonBlocking.class);
|
||||
uc.shutdownInput();
|
||||
uc.shutdownOutput();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessagesBlocking() throws Exception {
|
||||
doTestMessages(EchoBlocking.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessagesNonBlocking() throws Exception {
|
||||
doTestMessages(EchoNonBlocking.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetNullReadListener() throws Exception {
|
||||
doTestCheckClosed(SetNullReadListener.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetNullWriteListener() throws Exception {
|
||||
doTestCheckClosed(SetNullWriteListener.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetReadListenerTwice() throws Exception {
|
||||
doTestCheckClosed(SetReadListenerTwice.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetWriteListenerTwice() throws Exception {
|
||||
doTestCheckClosed(SetWriteListenerTwice.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstCallToOnWritePossible() throws Exception {
|
||||
doTestFixedResponse(FixedResponseNonBlocking.class);
|
||||
}
|
||||
|
||||
private void doTestCheckClosed(
|
||||
Class<? extends HttpUpgradeHandler> upgradeHandlerClass)
|
||||
throws Exception {
|
||||
UpgradeConnection conn = doUpgrade(upgradeHandlerClass);
|
||||
|
||||
Reader r = conn.getReader();
|
||||
int c = r.read();
|
||||
|
||||
Assert.assertEquals(-1, c);
|
||||
}
|
||||
|
||||
private void doTestFixedResponse(
|
||||
Class<? extends HttpUpgradeHandler> upgradeHandlerClass)
|
||||
throws Exception {
|
||||
UpgradeConnection conn = doUpgrade(upgradeHandlerClass);
|
||||
|
||||
Reader r = conn.getReader();
|
||||
int c = r.read();
|
||||
|
||||
Assert.assertEquals(FixedResponseNonBlocking.FIXED_RESPONSE, c);
|
||||
}
|
||||
|
||||
private void doTestMessages (
|
||||
Class<? extends HttpUpgradeHandler> upgradeHandlerClass)
|
||||
throws Exception {
|
||||
UpgradeConnection uc = doUpgrade(upgradeHandlerClass);
|
||||
PrintWriter pw = new PrintWriter(uc.getWriter());
|
||||
BufferedReader reader = uc.getReader();
|
||||
|
||||
pw.println(MESSAGE);
|
||||
pw.flush();
|
||||
|
||||
Thread.sleep(500);
|
||||
|
||||
pw.println(MESSAGE);
|
||||
pw.flush();
|
||||
|
||||
uc.shutdownOutput();
|
||||
|
||||
// Note: BufferedReader.readLine() strips new lines
|
||||
// ServletInputStream.readLine() does not strip new lines
|
||||
String response = reader.readLine();
|
||||
Assert.assertEquals(MESSAGE, response);
|
||||
response = reader.readLine();
|
||||
Assert.assertEquals(MESSAGE, response);
|
||||
|
||||
uc.shutdownInput();
|
||||
pw.close();
|
||||
}
|
||||
|
||||
|
||||
private UpgradeConnection doUpgrade(
|
||||
Class<? extends HttpUpgradeHandler> upgradeHandlerClass) throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
UpgradeServlet servlet = new UpgradeServlet(upgradeHandlerClass);
|
||||
Tomcat.addServlet(ctx, "servlet", servlet);
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
// Use raw socket so the necessary control is available after the HTTP
|
||||
// upgrade
|
||||
Socket socket =
|
||||
SocketFactory.getDefault().createSocket("localhost", getPort());
|
||||
|
||||
socket.setSoTimeout(5000);
|
||||
|
||||
UpgradeConnection uc = new UpgradeConnection(socket);
|
||||
|
||||
uc.getWriter().write("GET / HTTP/1.1" + CRLF);
|
||||
uc.getWriter().write("Host: whatever" + CRLF);
|
||||
uc.getWriter().write(CRLF);
|
||||
uc.getWriter().flush();
|
||||
|
||||
String status = uc.getReader().readLine();
|
||||
|
||||
Assert.assertNotNull(status);
|
||||
Assert.assertEquals("101", getStatusCode(status));
|
||||
|
||||
// Skip the remaining response headers
|
||||
String line = uc.getReader().readLine();
|
||||
while (line != null && line.length() > 0) {
|
||||
// Skip
|
||||
line = uc.getReader().readLine();
|
||||
}
|
||||
|
||||
return uc;
|
||||
}
|
||||
|
||||
private static class UpgradeServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Class<? extends HttpUpgradeHandler> upgradeHandlerClass;
|
||||
|
||||
public UpgradeServlet(Class<? extends HttpUpgradeHandler> upgradeHandlerClass) {
|
||||
this.upgradeHandlerClass = upgradeHandlerClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
|
||||
req.upgrade(upgradeHandlerClass);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpgradeConnection {
|
||||
private final Socket socket;
|
||||
private final Writer writer;
|
||||
private final BufferedReader reader;
|
||||
|
||||
public UpgradeConnection(Socket socket) {
|
||||
this.socket = socket;
|
||||
InputStream is;
|
||||
OutputStream os;
|
||||
try {
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalArgumentException(ioe);
|
||||
}
|
||||
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8);
|
||||
|
||||
this.writer = writer;
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public Writer getWriter() {
|
||||
return writer;
|
||||
}
|
||||
|
||||
public BufferedReader getReader() {
|
||||
return reader;
|
||||
}
|
||||
|
||||
public void shutdownOutput() throws IOException {
|
||||
writer.flush();
|
||||
socket.shutdownOutput();
|
||||
}
|
||||
|
||||
public void shutdownInput() throws IOException {
|
||||
socket.shutdownInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class EchoBlocking implements HttpUpgradeHandler {
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
|
||||
try (ServletInputStream sis = connection.getInputStream();
|
||||
ServletOutputStream sos = connection.getOutputStream()){
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = sis.read(buffer)) >= 0) {
|
||||
sos.write(buffer, 0, read);
|
||||
sos.flush();
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class EchoNonBlocking implements HttpUpgradeHandler {
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
ServletInputStream sis;
|
||||
ServletOutputStream sos;
|
||||
|
||||
try {
|
||||
sis = connection.getInputStream();
|
||||
sos = connection.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
|
||||
EchoListener echoListener = new EchoListener(sis, sos);
|
||||
sis.setReadListener(echoListener);
|
||||
sos.setWriteListener(echoListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
|
||||
private static class EchoListener implements ReadListener, WriteListener {
|
||||
|
||||
private final ServletInputStream sis;
|
||||
private final ServletOutputStream sos;
|
||||
private final byte[] buffer = new byte[8192];
|
||||
|
||||
public EchoListener(ServletInputStream sis, ServletOutputStream sos) {
|
||||
this.sis = sis;
|
||||
this.sos = sos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWritePossible() throws IOException {
|
||||
if (sis.isFinished()) {
|
||||
sis.close();
|
||||
sos.close();
|
||||
}
|
||||
while (sis.isReady()) {
|
||||
int read = sis.read(buffer);
|
||||
if (read > 0) {
|
||||
sos.write(buffer, 0, read);
|
||||
if (!sos.isReady()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataAvailable() throws IOException {
|
||||
if (sos.isReady()) {
|
||||
onWritePossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() throws IOException {
|
||||
if (sos.isReady()) {
|
||||
onWritePossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class SetNullReadListener implements HttpUpgradeHandler {
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
ServletInputStream sis;
|
||||
try {
|
||||
sis = connection.getInputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
sis.setReadListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class SetNullWriteListener implements HttpUpgradeHandler {
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
ServletOutputStream sos;
|
||||
try {
|
||||
sos = connection.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
sos.setWriteListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class SetReadListenerTwice implements HttpUpgradeHandler {
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
ServletInputStream sis;
|
||||
ServletOutputStream sos;
|
||||
try {
|
||||
sis = connection.getInputStream();
|
||||
sos = connection.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
sos.setWriteListener(new NoOpWriteListener());
|
||||
ReadListener rl = new NoOpReadListener();
|
||||
sis.setReadListener(rl);
|
||||
sis.setReadListener(rl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class SetWriteListenerTwice implements HttpUpgradeHandler {
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
ServletInputStream sis;
|
||||
ServletOutputStream sos;
|
||||
try {
|
||||
sis = connection.getInputStream();
|
||||
sos = connection.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
sis.setReadListener(new NoOpReadListener());
|
||||
WriteListener wl = new NoOpWriteListener();
|
||||
sos.setWriteListener(wl);
|
||||
sos.setWriteListener(wl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class FixedResponseNonBlocking implements HttpUpgradeHandler {
|
||||
|
||||
public static final char FIXED_RESPONSE = 'F';
|
||||
|
||||
private ServletInputStream sis;
|
||||
private ServletOutputStream sos;
|
||||
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
|
||||
try {
|
||||
sis = connection.getInputStream();
|
||||
sos = connection.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
|
||||
sis.setReadListener(new NoOpReadListener());
|
||||
sos.setWriteListener(new FixedResponseWriteListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
private class FixedResponseWriteListener extends NoOpWriteListener {
|
||||
@Override
|
||||
public void onWritePossible() {
|
||||
try {
|
||||
sos.write(FIXED_RESPONSE);
|
||||
sos.flush();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalStateException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class NoOpReadListener implements ReadListener {
|
||||
|
||||
@Override
|
||||
public void onDataAvailable() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAllDataRead() {
|
||||
// Always NO-OP for HTTP Upgrade
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class NoOpWriteListener implements WriteListener {
|
||||
|
||||
@Override
|
||||
public void onWritePossible() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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.http11.upgrade;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Writer;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.CompletionHandler;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpUpgradeHandler;
|
||||
import javax.servlet.http.WebConnection;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.apache.catalina.startup.TomcatBaseTest;
|
||||
import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
|
||||
import org.apache.tomcat.util.net.SSLSupport;
|
||||
import org.apache.tomcat.util.net.SocketEvent;
|
||||
import org.apache.tomcat.util.net.SocketWrapperBase;
|
||||
import org.apache.tomcat.util.net.SocketWrapperBase.BlockingMode;
|
||||
import org.apache.tomcat.util.net.SocketWrapperBase.CompletionState;
|
||||
|
||||
public class TestUpgradeInternalHandler extends TomcatBaseTest {
|
||||
|
||||
private static final String MESSAGE = "This is a test.";
|
||||
|
||||
@Test
|
||||
public void testUpgradeInternal() throws Exception {
|
||||
Assume.assumeTrue(
|
||||
"Only supported on NIO 2",
|
||||
getTomcatInstance().getConnector().getProtocolHandlerClassName().contains("Nio2"));
|
||||
|
||||
UpgradeConnection uc = doUpgrade(EchoAsync.class);
|
||||
PrintWriter pw = new PrintWriter(uc.getWriter());
|
||||
BufferedReader reader = uc.getReader();
|
||||
|
||||
// Add extra sleep to avoid completing inline
|
||||
Thread.sleep(500);
|
||||
pw.println(MESSAGE);
|
||||
pw.flush();
|
||||
Thread.sleep(500);
|
||||
uc.shutdownOutput();
|
||||
|
||||
// Note: BufferedReader.readLine() strips new lines
|
||||
// ServletInputStream.readLine() does not strip new lines
|
||||
String response = reader.readLine();
|
||||
Assert.assertEquals(MESSAGE, response);
|
||||
|
||||
uc.shutdownInput();
|
||||
pw.close();
|
||||
}
|
||||
|
||||
private UpgradeConnection doUpgrade(
|
||||
Class<? extends HttpUpgradeHandler> upgradeHandlerClass) throws Exception {
|
||||
// Setup Tomcat instance
|
||||
Tomcat tomcat = getTomcatInstance();
|
||||
|
||||
// No file system docBase required
|
||||
Context ctx = tomcat.addContext("", null);
|
||||
|
||||
UpgradeServlet servlet = new UpgradeServlet(upgradeHandlerClass);
|
||||
Tomcat.addServlet(ctx, "servlet", servlet);
|
||||
ctx.addServletMappingDecoded("/", "servlet");
|
||||
|
||||
tomcat.start();
|
||||
|
||||
// Use raw socket so the necessary control is available after the HTTP
|
||||
// upgrade
|
||||
Socket socket =
|
||||
SocketFactory.getDefault().createSocket("localhost", getPort());
|
||||
|
||||
socket.setSoTimeout(5000);
|
||||
|
||||
UpgradeConnection uc = new UpgradeConnection(socket);
|
||||
|
||||
uc.getWriter().write("GET / HTTP/1.1" + CRLF);
|
||||
uc.getWriter().write("Host: whatever" + CRLF);
|
||||
uc.getWriter().write(CRLF);
|
||||
uc.getWriter().flush();
|
||||
|
||||
String status = uc.getReader().readLine();
|
||||
|
||||
Assert.assertNotNull(status);
|
||||
Assert.assertEquals("101", getStatusCode(status));
|
||||
|
||||
// Skip the remaining response headers
|
||||
String line = uc.getReader().readLine();
|
||||
while (line != null && line.length() > 0) {
|
||||
// Skip
|
||||
line = uc.getReader().readLine();
|
||||
}
|
||||
|
||||
return uc;
|
||||
}
|
||||
|
||||
private static class UpgradeServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Class<? extends HttpUpgradeHandler> upgradeHandlerClass;
|
||||
|
||||
public UpgradeServlet(Class<? extends HttpUpgradeHandler> upgradeHandlerClass) {
|
||||
this.upgradeHandlerClass = upgradeHandlerClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
|
||||
req.upgrade(upgradeHandlerClass);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpgradeConnection {
|
||||
private final Socket socket;
|
||||
private final Writer writer;
|
||||
private final BufferedReader reader;
|
||||
|
||||
public UpgradeConnection(Socket socket) {
|
||||
this.socket = socket;
|
||||
InputStream is;
|
||||
OutputStream os;
|
||||
try {
|
||||
is = socket.getInputStream();
|
||||
os = socket.getOutputStream();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalArgumentException(ioe);
|
||||
}
|
||||
|
||||
BufferedReader reader =
|
||||
new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8);
|
||||
|
||||
this.writer = writer;
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public Writer getWriter() {
|
||||
return writer;
|
||||
}
|
||||
|
||||
public BufferedReader getReader() {
|
||||
return reader;
|
||||
}
|
||||
|
||||
public void shutdownOutput() throws IOException {
|
||||
writer.flush();
|
||||
socket.shutdownOutput();
|
||||
}
|
||||
|
||||
public void shutdownInput() throws IOException {
|
||||
socket.shutdownInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class EchoAsync implements InternalHttpUpgradeHandler {
|
||||
private SocketWrapperBase<?> wrapper;
|
||||
@Override
|
||||
public void init(WebConnection connection) {
|
||||
System.out.println("Init: " + connection);
|
||||
// Arbitrarily located in the init, could be in the initial read event, asynchronous, etc.
|
||||
// Note: the completion check used will not call the completion handler if the IO completed inline and without error.
|
||||
// Using a completion check that always calls complete would be easier here since the action is the same even with inline completion.
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
CompletionState state = wrapper.read(BlockingMode.NON_BLOCK, 10, TimeUnit.SECONDS, null, SocketWrapperBase.READ_DATA, new CompletionHandler<Long, Void>() {
|
||||
@Override
|
||||
public void completed(Long result, Void attachment) {
|
||||
System.out.println("Read: " + result.longValue());
|
||||
write(buffer);
|
||||
}
|
||||
@Override
|
||||
public void failed(Throwable exc, Void attachment) {
|
||||
exc.printStackTrace();
|
||||
}
|
||||
}, buffer);
|
||||
System.out.println("CompletionState: " + state);
|
||||
if (state == CompletionState.INLINE) {
|
||||
write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(ByteBuffer buffer) {
|
||||
buffer.flip();
|
||||
CompletionState state = wrapper.write(BlockingMode.BLOCK, 10, TimeUnit.SECONDS, null, SocketWrapperBase.COMPLETE_WRITE, new CompletionHandler<Long, Void>() {
|
||||
@Override
|
||||
public void completed(Long result, Void attachment) {
|
||||
System.out.println("Write: " + result.longValue());
|
||||
}
|
||||
@Override
|
||||
public void failed(Throwable exc, Void attachment) {
|
||||
exc.printStackTrace();
|
||||
}
|
||||
}, buffer);
|
||||
System.out.println("CompletionState: " + state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketState upgradeDispatch(SocketEvent status) {
|
||||
System.out.println("Processing: " + status);
|
||||
switch (status) {
|
||||
case OPEN_READ:
|
||||
// Note: there's always an initial read event at the moment (reading should be skipped since it ends up in the internal buffer)
|
||||
break;
|
||||
case OPEN_WRITE:
|
||||
break;
|
||||
case STOP:
|
||||
case DISCONNECT:
|
||||
case ERROR:
|
||||
case TIMEOUT:
|
||||
case CONNECT_FAIL:
|
||||
return SocketState.CLOSED;
|
||||
}
|
||||
return SocketState.UPGRADED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void timeoutAsync(long now) {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSocketWrapper(SocketWrapperBase<?> wrapper) {
|
||||
this.wrapper = wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSslSupport(SSLSupport sslSupport) {
|
||||
// NO-OP
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1353
test/org/apache/coyote/http2/Http2TestBase.java
Normal file
1353
test/org/apache/coyote/http2/Http2TestBase.java
Normal file
File diff suppressed because it is too large
Load Diff
129
test/org/apache/coyote/http2/TestAbortedUpload.java
Normal file
129
test/org/apache/coyote/http2/TestAbortedUpload.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
test/org/apache/coyote/http2/TestAbstractStream.java
Normal file
286
test/org/apache/coyote/http2/TestAbstractStream.java
Normal 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());
|
||||
}
|
||||
}
|
||||
277
test/org/apache/coyote/http2/TestAsync.java
Normal file
277
test/org/apache/coyote/http2/TestAsync.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
154
test/org/apache/coyote/http2/TestAsyncFlush.java
Normal file
154
test/org/apache/coyote/http2/TestAsyncFlush.java
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
226
test/org/apache/coyote/http2/TestAsyncTimeout.java
Normal file
226
test/org/apache/coyote/http2/TestAsyncTimeout.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
test/org/apache/coyote/http2/TestByteUtil.java
Normal file
39
test/org/apache/coyote/http2/TestByteUtil.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
146
test/org/apache/coyote/http2/TestHpack.java
Normal file
146
test/org/apache/coyote/http2/TestHpack.java
Normal 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));
|
||||
}
|
||||
}
|
||||
174
test/org/apache/coyote/http2/TestHttp2InitialConnection.java
Normal file
174
test/org/apache/coyote/http2/TestHttp2InitialConnection.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
550
test/org/apache/coyote/http2/TestHttp2Limits.java
Normal file
550
test/org/apache/coyote/http2/TestHttp2Limits.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
180
test/org/apache/coyote/http2/TestHttp2Section_3_2.java
Normal file
180
test/org/apache/coyote/http2/TestHttp2Section_3_2.java
Normal 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.
|
||||
}
|
||||
39
test/org/apache/coyote/http2/TestHttp2Section_3_5.java
Normal file
39
test/org/apache/coyote/http2/TestHttp2Section_3_5.java
Normal 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();
|
||||
}
|
||||
}
|
||||
72
test/org/apache/coyote/http2/TestHttp2Section_4_1.java
Normal file
72
test/org/apache/coyote/http2/TestHttp2Section_4_1.java
Normal 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());
|
||||
}
|
||||
}
|
||||
131
test/org/apache/coyote/http2/TestHttp2Section_4_2.java
Normal file
131
test/org/apache/coyote/http2/TestHttp2Section_4_2.java
Normal 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]"));
|
||||
}
|
||||
}
|
||||
92
test/org/apache/coyote/http2/TestHttp2Section_4_3.java
Normal file
92
test/org/apache/coyote/http2/TestHttp2Section_4_3.java
Normal 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);
|
||||
}
|
||||
}
|
||||
288
test/org/apache/coyote/http2/TestHttp2Section_5_1.java
Normal file
288
test/org/apache/coyote/http2/TestHttp2Section_5_1.java
Normal 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);
|
||||
}
|
||||
}
|
||||
120
test/org/apache/coyote/http2/TestHttp2Section_5_2.java
Normal file
120
test/org/apache/coyote/http2/TestHttp2Section_5_2.java
Normal 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);
|
||||
}
|
||||
}
|
||||
249
test/org/apache/coyote/http2/TestHttp2Section_5_3.java
Normal file
249
test/org/apache/coyote/http2/TestHttp2Section_5_3.java
Normal 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;
|
||||
}
|
||||
}
|
||||
96
test/org/apache/coyote/http2/TestHttp2Section_5_5.java
Normal file
96
test/org/apache/coyote/http2/TestHttp2Section_5_5.java
Normal 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);
|
||||
}
|
||||
}
|
||||
162
test/org/apache/coyote/http2/TestHttp2Section_6_1.java
Normal file
162
test/org/apache/coyote/http2/TestHttp2Section_6_1.java
Normal 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());
|
||||
}
|
||||
}
|
||||
107
test/org/apache/coyote/http2/TestHttp2Section_6_2.java
Normal file
107
test/org/apache/coyote/http2/TestHttp2Section_6_2.java
Normal 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());
|
||||
}
|
||||
}
|
||||
86
test/org/apache/coyote/http2/TestHttp2Section_6_3.java
Normal file
86
test/org/apache/coyote/http2/TestHttp2Section_6_3.java
Normal 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());
|
||||
}
|
||||
}
|
||||
79
test/org/apache/coyote/http2/TestHttp2Section_6_4.java
Normal file
79
test/org/apache/coyote/http2/TestHttp2Section_6_4.java
Normal 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());
|
||||
}
|
||||
}
|
||||
127
test/org/apache/coyote/http2/TestHttp2Section_6_5.java
Normal file
127
test/org/apache/coyote/http2/TestHttp2Section_6_5.java
Normal 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.
|
||||
}
|
||||
83
test/org/apache/coyote/http2/TestHttp2Section_6_7.java
Normal file
83
test/org/apache/coyote/http2/TestHttp2Section_6_7.java
Normal 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);
|
||||
}
|
||||
}
|
||||
87
test/org/apache/coyote/http2/TestHttp2Section_6_8.java
Normal file
87
test/org/apache/coyote/http2/TestHttp2Section_6_8.java
Normal 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
|
||||
}
|
||||
282
test/org/apache/coyote/http2/TestHttp2Section_6_9.java
Normal file
282
test/org/apache/coyote/http2/TestHttp2Section_6_9.java
Normal 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());
|
||||
|
||||
}
|
||||
}
|
||||
217
test/org/apache/coyote/http2/TestHttp2Section_8_1.java
Normal file
217
test/org/apache/coyote/http2/TestHttp2Section_8_1.java
Normal 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());
|
||||
}
|
||||
}
|
||||
107
test/org/apache/coyote/http2/TestHttp2Timeouts.java
Normal file
107
test/org/apache/coyote/http2/TestHttp2Timeouts.java
Normal 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());
|
||||
}
|
||||
}
|
||||
72
test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java
Normal file
72
test/org/apache/coyote/http2/TestHttp2UpgradeHandler.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
||||
98
test/org/apache/coyote/http2/TestStream.java
Normal file
98
test/org/apache/coyote/http2/TestStream.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
226
test/org/apache/coyote/http2/TestStreamProcessor.java
Normal file
226
test/org/apache/coyote/http2/TestStreamProcessor.java
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
187
test/org/apache/coyote/http2/TestStreamQueryString.java
Normal file
187
test/org/apache/coyote/http2/TestStreamQueryString.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user