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

View File

@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote;
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)));
}
}

View 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();
}
}
}

View 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");
}
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View File

@@ -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();
}
}

View 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;
}
}
}

View 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());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.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));
}
}

View 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
}
}
}

View 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
}
}
}

View File

@@ -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
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
public class TestAbortedUpload extends Http2TestBase {
@Test
public void testAbortedRequest() throws Exception {
http2Connect();
http2Protocol.setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
int bodySize = 8192;
int bodyCount = 20;
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(bodySize);
byte[] trailerFrameHeader = new byte[9];
ByteBuffer trailerPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload,
null, trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
for (int i = 0; i < bodyCount; i++) {
writeFrame(dataFrameHeader, dataPayload);
}
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
// The actual response depends on timing issues. Particularly how much
// data is transferred in StreamInputBuffer inBuffer to outBuffer on the
// first read.
while (output.getTrace().length() == 0) {
try {
parser.readFrame(true);
if ("3-RST-[3]\n".equals(output.getTrace())) {
output.clearTrace();
}
} catch (IOException ioe) {
// Might not be any further frames after the reset
break;
}
}
if (output.getTrace().startsWith("0-WindowSize-[")) {
String trace = output.getTrace();
int size = Integer.parseInt(trace.substring(14, trace.length() - 2));
output.clearTrace();
// Window updates always come in pairs
parser.readFrame(true);
Assert.assertEquals("3-WindowSize-[" + size + "]\n", output.getTrace());
}
}
@Override
protected void configureAndStartWebApplication() throws LifecycleException {
Tomcat tomcat = getTomcatInstance();
// Retain '/simple' url-pattern since it enables code re-use
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "abort", new AbortServlet());
ctxt.addServletMappingDecoded("/simple", "abort");
tomcat.start();
}
private static class AbortServlet extends SimpleServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Read upto 128 bytes and then return a 403 response
InputStream is = req.getInputStream();
byte[] buf = new byte[128];
int toRead = 128;
int read = is.read(buf);
while (read != -1 && toRead > 0) {
toRead -= read;
read = is.read(buf);
}
if (toRead == 0) {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
} else {
resp.setStatus(HttpServletResponse.SC_OK);
}
}
}
}

View File

@@ -0,0 +1,286 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/*
* This tests use A=1, B=2, etc to map stream IDs to the names used in the
* figures.
*/
public class TestAbstractStream {
@Test
public void testDependenciesFig3() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
// Action
d.rePrioritise(a, false, 16);
// Check parents
Assert.assertEquals(handler, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(a, d.getParentStream());
// Check children
Assert.assertEquals(3, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertTrue(a.getChildStreams().contains(d));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, c.getChildStreams().size());
Assert.assertEquals(0, d.getChildStreams().size());
}
@Test
public void testDependenciesFig4() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
// Action
d.rePrioritise(a, true, 16);
// Check parents
Assert.assertEquals(handler, a.getParentStream());
Assert.assertEquals(d, b.getParentStream());
Assert.assertEquals(d, c.getParentStream());
Assert.assertEquals(a, d.getParentStream());
// Check children
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(d));
Assert.assertEquals(2, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(b));
Assert.assertTrue(d.getChildStreams().contains(c));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, c.getChildStreams().size());
}
@Test
public void testDependenciesFig5NonExclusive() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(c, false, 16);
e.rePrioritise(c, false, 16);
f.rePrioritise(d, false, 16);
// Action
a.rePrioritise(d, false, 16);
// Check parents
Assert.assertEquals(handler, d.getParentStream());
Assert.assertEquals(d, f.getParentStream());
Assert.assertEquals(d, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(c, e.getParentStream());
// Check children
Assert.assertEquals(2, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(a));
Assert.assertTrue(d.getChildStreams().contains(f));
Assert.assertEquals(0, f.getChildStreams().size());
Assert.assertEquals(2, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(e));
Assert.assertEquals(0, e.getChildStreams().size());
}
@Test
public void testDependenciesFig5Exclusive() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(c, false, 16);
e.rePrioritise(c, false, 16);
f.rePrioritise(d, false, 16);
// Action
a.rePrioritise(d, true, 16);
// Check parents
Assert.assertEquals(handler, d.getParentStream());
Assert.assertEquals(d, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(a, c.getParentStream());
Assert.assertEquals(a, f.getParentStream());
Assert.assertEquals(c, e.getParentStream());
// Check children
Assert.assertEquals(1, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(a));
Assert.assertEquals(3, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertTrue(a.getChildStreams().contains(c));
Assert.assertTrue(a.getChildStreams().contains(f));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(0, f.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(e));
Assert.assertEquals(0, e.getChildStreams().size());
}
@Test
public void testCircular01() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(b, false, 16);
// Action
a.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(c, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(a));
}
@Test
public void testCircular02() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(2), handler);
Stream c = new Stream(Integer.valueOf(3), handler);
Stream d = new Stream(Integer.valueOf(4), handler);
Stream e = new Stream(Integer.valueOf(5), handler);
Stream f = new Stream(Integer.valueOf(6), handler);
b.rePrioritise(a, false, 16);
c.rePrioritise(b, false, 16);
e.rePrioritise(d, false, 16);
f.rePrioritise(e, false, 16);
// Action
a.rePrioritise(f, false, 16);
d.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(f, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
Assert.assertEquals(c, d.getParentStream());
Assert.assertEquals(d, e.getParentStream());
Assert.assertEquals(e, f.getParentStream());
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(0, b.getChildStreams().size());
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(d));
Assert.assertEquals(1, d.getChildStreams().size());
Assert.assertTrue(d.getChildStreams().contains(e));
Assert.assertEquals(1, e.getChildStreams().size());
Assert.assertTrue(e.getChildStreams().contains(f));
Assert.assertEquals(1, f.getChildStreams().size());
Assert.assertTrue(f.getChildStreams().contains(a));
}
// https://bz.apache.org/bugzilla/show_bug.cgi?id=61682
@Test
public void testCircular03() {
// Setup
Http2UpgradeHandler handler = new Http2UpgradeHandler(new Http2Protocol(), null, null);
Stream a = new Stream(Integer.valueOf(1), handler);
Stream b = new Stream(Integer.valueOf(3), handler);
Stream c = new Stream(Integer.valueOf(5), handler);
Stream d = new Stream(Integer.valueOf(7), handler);
// Action
b.rePrioritise(a, false, 16);
c.rePrioritise(a, false, 16);
d.rePrioritise(b, false, 16);
c.rePrioritise(handler, false, 16);
a.rePrioritise(c, false, 16);
// Check parents
Assert.assertEquals(c, a.getParentStream());
Assert.assertEquals(a, b.getParentStream());
Assert.assertEquals(handler, c.getParentStream());
Assert.assertEquals(b, d.getParentStream());
// This triggers the StackOverflowError
Assert.assertTrue(c.isDescendant(d));
// Check children
Assert.assertEquals(1, handler.getChildStreams().size());
Assert.assertTrue(handler.getChildStreams().contains(c));
Assert.assertEquals(1, c.getChildStreams().size());
Assert.assertTrue(c.getChildStreams().contains(a));
Assert.assertEquals(1, a.getChildStreams().size());
Assert.assertTrue(a.getChildStreams().contains(b));
Assert.assertEquals(1, b.getChildStreams().size());
Assert.assertTrue(b.getChildStreams().contains(d));
Assert.assertEquals(0, d.getChildStreams().size());
}
}

View File

@@ -0,0 +1,277 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
/*
* Based on
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62614
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62620
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62628
*/
@RunWith(Parameterized.class)
public class TestAsync extends Http2TestBase {
private static final int BLOCK_SIZE = 0x8000;
@Parameterized.Parameters(name = "{index}: expandConnectionFirst[{0}], " +
"connectionUnlimited[{1}], streamUnlimited[{2}], useNonContainerThreadForWrite[{3}]," +
"largeInitialWindow[{4}]")
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
for (Boolean expandConnectionFirst : booleans) {
for (Boolean connectionUnlimited : booleans) {
for (Boolean streamUnlimited : booleans) {
for (Boolean useNonContainerThreadForWrite : booleans) {
for (Boolean largeInitialWindow : booleans) {
parameterSets.add(new Object[] {
expandConnectionFirst, connectionUnlimited, streamUnlimited,
useNonContainerThreadForWrite, largeInitialWindow
});
}
}
}
}
}
return parameterSets;
}
private final boolean expandConnectionFirst;
private final boolean connectionUnlimited;
private final boolean streamUnlimited;
private final boolean useNonContainerThreadForWrite;
private final boolean largeInitialWindow;
public TestAsync(boolean expandConnectionFirst, boolean connectionUnlimited,
boolean streamUnlimited, boolean useNonContainerThreadForWrite,
boolean largeInitialWindow) {
this.expandConnectionFirst = expandConnectionFirst;
this.connectionUnlimited = connectionUnlimited;
this.streamUnlimited = streamUnlimited;
this.useNonContainerThreadForWrite = useNonContainerThreadForWrite;
this.largeInitialWindow = largeInitialWindow;
}
@Test
public void testEmptyWindow() throws Exception {
int blockCount = 8;
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async",
new AsyncServlet(blockCount, useNonContainerThreadForWrite));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
int startingWindowSize;
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after initial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
if (largeInitialWindow) {
startingWindowSize = ((1 << 17) - 1);
SettingValue sv =
new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), startingWindowSize);
sendSettings(0, false, sv);
// Test code assumes connection window and stream window size are the same at the start
sendWindowUpdate(0, startingWindowSize - ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
} else {
startingWindowSize = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
}
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
if (connectionUnlimited) {
// Effectively unlimited for this test
sendWindowUpdate(0, blockCount * BLOCK_SIZE * 2);
}
if (streamUnlimited) {
// Effectively unlimited for this test
sendWindowUpdate(3, blockCount * BLOCK_SIZE * 2);
}
// Headers
parser.readFrame(true);
// Body
if (!connectionUnlimited || !streamUnlimited) {
while (output.getBytesRead() < startingWindowSize) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(startingWindowSize, output.getBytesRead());
// Increase the Window size (50% of total body)
int windowSizeIncrease = blockCount * BLOCK_SIZE / 2;
if (expandConnectionFirst) {
sendWindowUpdate(0, windowSizeIncrease);
sendWindowUpdate(3, windowSizeIncrease);
} else {
sendWindowUpdate(3, windowSizeIncrease);
sendWindowUpdate(0, windowSizeIncrease);
}
while (output.getBytesRead() < startingWindowSize + windowSizeIncrease) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(startingWindowSize + windowSizeIncrease, output.getBytesRead());
// Increase the Window size
if (expandConnectionFirst) {
sendWindowUpdate(0, windowSizeIncrease);
sendWindowUpdate(3, windowSizeIncrease);
} else {
sendWindowUpdate(3, windowSizeIncrease);
sendWindowUpdate(0, windowSizeIncrease);
}
}
while (!output.getTrace().endsWith("3-EndOfStream\n")) {
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals((long) blockCount * BLOCK_SIZE, output.getBytesRead());
}
public static class AsyncServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int blockLimit;
private final boolean useNonContainerThreadForWrite;
private final transient ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private transient volatile Future<?> future;
public AsyncServlet(int blockLimit, boolean useNonContainerThreadForWrite) {
this.blockLimit = blockLimit;
this.useNonContainerThreadForWrite = useNonContainerThreadForWrite;
}
/*
* Not thread-safe. OK for this test. NOt OK for use in the real world.
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/binary");
final ServletOutputStream output = response.getOutputStream();
output.setWriteListener(new WriteListener() {
// Intermittent CI errors were observed where the response body
// was exactly one block too small. Use an AtomicInteger to be
// sure blockCount is thread-safe.
final AtomicInteger blockCount = new AtomicInteger(0);
byte[] bytes = new byte[BLOCK_SIZE];
@Override
public void onWritePossible() throws IOException {
if (useNonContainerThreadForWrite) {
future = scheduler.schedule(new Runnable() {
@Override
public void run() {
try {
write();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}, 200, TimeUnit.MILLISECONDS);
} else {
write();
}
}
private void write() throws IOException {
while (output.isReady()) {
blockCount.incrementAndGet();
output.write(bytes);
if (blockCount.get() == blockLimit) {
asyncContext.complete();
scheduler.shutdown();
return;
}
}
}
@Override
public void onError(Throwable t) {
if (future != null) {
future.cancel(false);
}
t.printStackTrace();
}
});
}
}
}

View File

@@ -0,0 +1,154 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.servlet.AsyncContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
/*
* Based on
* https://bz.apache.org/bugzilla/show_bug.cgi?id=62635
*
* Note: Calling blocking I/O methods (such as flushBuffer()) during
* non-blocking I/O is explicitly called out as illegal in the Servlet
* specification but also goes on to say the behaviour if such a call is
* made is undefined. Which means it is OK if the call works as expected
* (a non-blocking flush is triggered) :).
* If any of these tests fail, that should not block a release since -
* while the specification allows this to work - it doesn't require that
* it does work.
*/
public class TestAsyncFlush extends Http2TestBase {
private static final int BLOCK_SIZE = 1024;
@Test
public void testFlush() throws Exception {
int blockCount = 2048;
int targetSize = BLOCK_SIZE * blockCount;
int totalWindow = ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncFlushServlet(blockCount));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after intial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
// Send request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
// Body
while (output.getBytesRead() < targetSize ) {
if (output.getBytesRead() == totalWindow) {
sendWindowUpdate(3, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
sendWindowUpdate(0, ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE);
totalWindow += ConnectionSettingsBase.DEFAULT_INITIAL_WINDOW_SIZE;
}
parser.readFrame(true);
}
// Check that the right number of bytes were received
Assert.assertEquals(targetSize, output.getBytesRead());
}
public static class AsyncFlushServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final int blockLimit;
public AsyncFlushServlet(int blockLimit) {
this.blockLimit = blockLimit;
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws IOException {
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/binary");
final ServletOutputStream output = response.getOutputStream();
output.setWriteListener(new WriteListener() {
int blockCount;
byte[] bytes = new byte[BLOCK_SIZE];
@Override
public void onWritePossible() throws IOException {
while (output.isReady()) {
blockCount++;
output.write(bytes);
if (blockCount % 5 == 0) {
response.flushBuffer();
}
if (blockCount == blockLimit) {
asyncContext.complete();
return;
}
}
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
});
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
public class TestAsyncTimeout extends Http2TestBase {
@Test
public void testTimeout() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
// This is the target of the HTTP/2 upgrade request
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
// This is the servlet that does that actual test
// This latch is used to signal that that async thread used by the test
// has ended. It isn;t essential to the test but it allows the test to
// complete without Tmcat logging an error about a still running thread.
CountDownLatch latch = new CountDownLatch(1);
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncTimeoutServlet(latch));
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
// Reset connection window size after initial response
sendWindowUpdate(0, SimpleServlet.CONTENT_LENGTH);
// Include the response body in the trace so we can check for the PASS /
// FAIL text.
output.setTraceBody(true);
// Send request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
// Body
parser.readFrame(true);
// Check that the expected text was received
String trace = output.getTrace();
Assert.assertTrue(trace, trace.contains("PASS"));
latch.await(10, TimeUnit.SECONDS);
}
public static class AsyncTimeoutServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final CountDownLatch latch;
public AsyncTimeoutServlet(CountDownLatch latch) {
this.latch = latch;
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
// The idea of this test is that the timeout kicks in after 2
// seconds and stops the async thread early rather than letting it
// complete the full 5 seconds of processing.
final AsyncContext asyncContext = request.startAsync();
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
// Only want to call complete() once (else we get stack traces in
// the logs so use this to track when complete() is called).
AtomicBoolean completeCalled = new AtomicBoolean(false);
Ticker ticker = new Ticker(asyncContext, completeCalled);
TimeoutListener listener = new TimeoutListener(latch, ticker, completeCalled);
asyncContext.addListener(listener);
asyncContext.setTimeout(2000);
ticker.start();
}
}
private static class Ticker extends Thread {
private final AsyncContext asyncContext;
private final AtomicBoolean completeCalled;
private volatile boolean running = true;
public Ticker(AsyncContext asyncContext, AtomicBoolean completeCalled) {
this.asyncContext = asyncContext;
this.completeCalled = completeCalled;
}
public void end() {
running = false;
}
@Override
public void run() {
try {
PrintWriter pw = asyncContext.getResponse().getWriter();
int counter = 0;
// If the test works running will be set too false before
// counter reaches 50.
while (running && counter < 50) {
Thread.sleep(100);
counter++;
pw.print("Tick " + counter);
}
// Need to call complete() here if the test fails but complete()
// should have been called by the listener. Use the flag to make
// sure we only call complete once.
if (completeCalled.compareAndSet(false, true)) {
asyncContext.complete();
}
} catch (IOException | InterruptedException e) {
// Ignore
}
}
}
private static class TimeoutListener implements AsyncListener {
private final AtomicBoolean ended = new AtomicBoolean(false);
private final CountDownLatch latch;
private final Ticker ticker;
private final AtomicBoolean completeCalled;
public TimeoutListener(CountDownLatch latch, Ticker ticker, AtomicBoolean completeCalled) {
this.latch = latch;
this.ticker = ticker;
this.completeCalled = completeCalled;
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
ticker.end();
if (ended.compareAndSet(false, true)) {
PrintWriter pw = event.getAsyncContext().getResponse().getWriter();
pw.write("PASS");
pw.flush();
// If the timeout fires we should always need to call complete()
// here but use the flag to be safe.
if (completeCalled.compareAndSet(false, true)) {
event.getAsyncContext().complete();
}
}
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onError(AsyncEvent event) throws IOException {
// NO-OP
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
if (ended.compareAndSet(false, true)) {
PrintWriter pw = event.getAsyncContext().getResponse().getWriter();
pw.write("FAIL");
pw.flush();
}
try {
// Wait for the async thread to end before we signal that the
// test is complete. This avoids logging an exception about a
// still running thread when the unit test shuts down.
ticker.join();
latch.countDown();
} catch (InterruptedException e) {
// Ignore
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
public class TestByteUtil {
@Test
public void testGet31Bits() {
byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
int result = ByteUtil.get31Bits(input, 0);
Assert.assertEquals(0x7fffffff, result);
}
@Test
public void testGetFourBytes() {
byte[] input = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
long result = ByteUtil.getFourBytes(input, 0);
Assert.assertEquals(0xffffffffL, result);
}
}

View File

@@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
import org.apache.tomcat.util.http.MimeHeaders;
public class TestHpack {
@Test
public void testEncode() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("header1").setString("value1");
headers.setValue(":status").setString("200");
headers.setValue("header2").setString("value2");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
// Size is supposed to be 33 without huffman, or 27 with it
// TODO: use the HpackHeaderFunction to enable huffman predictably
Assert.assertEquals(27, output.remaining());
output.clear();
encoder.encode(headers, output);
output.flip();
// Size is now 3 after using the table
Assert.assertEquals(3, output.remaining());
}
@Test
public void testDecode() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("header1").setString("value1");
headers.setValue(":status").setString("200");
headers.setValue("header2").setString("value2");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
// Redo (table is supposed to be updated)
output.clear();
encoder.encode(headers, output);
output.flip();
headers2.recycle();
Assert.assertEquals(3, output.remaining());
// Check that the decoder is using the table right
decoder.decode(output);
Assert.assertEquals("value2", headers2.getHeader("header2"));
}
private static class HeadersListener implements HpackDecoder.HeaderEmitter {
private final MimeHeaders headers;
public HeadersListener(MimeHeaders headers) {
this.headers = headers;
}
@Override
public void emitHeader(String name, String value) {
headers.setValue(name).setString(value);
}
@Override
public void setHeaderException(StreamException streamException) {
// NO-OP
}
@Override
public void validateHeaders() throws StreamException {
// NO-OP
}
}
@Test
public void testHeaderValueBug60451() throws HpackException {
doTestHeaderValueBug60451("fooébar");
}
@Test
public void testHeaderValueFullRange() {
for (int i = 0; i < 256; i++) {
// Skip the control characters except VTAB
if (i == 9 || i > 31 && i < 127 || i > 127) {
try {
doTestHeaderValueBug60451("foo" + Character.toString((char) i) + "bar");
} catch (Exception e) {
e.printStackTrace();
Assert.fail(e.getMessage() + "[" + i + "]");
}
}
}
}
@Test(expected=HpackException.class)
public void testExcessiveStringLiteralPadding() throws Exception {
MimeHeaders headers = new MimeHeaders();
headers.setValue("X-test").setString("foobar");
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
// Hack the output buffer to extend the EOS marker for the header value
// by another byte
output.array()[7] = (byte) -122;
output.put((byte) -1);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
}
private void doTestHeaderValueBug60451(String filename) throws HpackException {
String headerName = "Content-Disposition";
String headerValue = "attachment;filename=\"" + filename + "\"";
MimeHeaders headers = new MimeHeaders();
headers.setValue(headerName).setString(headerValue);
ByteBuffer output = ByteBuffer.allocate(512);
HpackEncoder encoder = new HpackEncoder();
encoder.encode(headers, output);
output.flip();
MimeHeaders headers2 = new MimeHeaders();
HpackDecoder decoder = new HpackDecoder();
decoder.setHeaderEmitter(new HeadersListener(headers2));
decoder.decode(output);
Assert.assertEquals(headerValue, headers2.getHeader(headerName));
}
}

View File

@@ -0,0 +1,174 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.tomcat.util.res.StringManager;
public class TestHttp2InitialConnection extends Http2TestBase {
private TestData testData;
@Test
public void testValidHostHeader() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
hostHeaders.add("localhost:8080");
testData = new TestData(hostHeaders, 200);
http2Connect();
}
@Test
public void testMultipleHostHeaders() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
hostHeaders.add("localhost:8080");
hostHeaders.add("localhost:8081");
testData = new TestData(hostHeaders, 400);
http2Connect();
}
@Test
public void testNoHostHeader() throws Exception {
List<String> hostHeaders = new ArrayList<>(1);
testData = new TestData(hostHeaders, 400);
http2Connect();
}
@Override
protected void doHttpUpgrade(String connection, String upgrade, String settings,
boolean validate) throws IOException {
StringBuilder request = new StringBuilder();
request.append("GET /simple HTTP/1.1\r\n");
for (String hostHeader : testData.getHostHeaders()) {
request.append("Host: ");
request.append(hostHeader);
request.append("\r\n");
}
// Connection
request.append("Connection: ");
request.append(connection);
request.append("\r\n");
// Upgrade
request.append("Upgrade: ");
request.append(upgrade);
request.append("\r\n");
// Settings
request.append(settings);
// Locale - Force the en Locale else the i18n on the error page changes
// the size of the response body and that triggers a failure as the test
// checks the exact response length
request.append("Accept-Language: en\r\n");
// Request terminator
request.append("\r\n");
byte[] upgradeRequest = request.toString().getBytes(StandardCharsets.ISO_8859_1);
os.write(upgradeRequest);
os.flush();
if (validate) {
Assert.assertTrue("Failed to read HTTP Upgrade response",
readHttpUpgradeResponse());
}
}
@Override
protected String getResponseBodyFrameTrace(int streamId, String body) {
if (testData.getExpectedStatus() == 200) {
return super.getResponseBodyFrameTrace(streamId, body);
} else if (testData.getExpectedStatus() == 400) {
/*
* Need to be careful here. The test wants the exact content length
* in bytes.
* This will vary depending on where the test is run due to:
* - The length of the version string that appears once in the error
* page
* - The status header uses a UTF-8 EN dash. When running in an IDE
* the UTF-8 properties files will be used directly rather than
* after native2ascii conversion.
*
* Note: The status header appears twice in the error page.
*/
int serverInfoLength = ServerInfo.getServerInfo().getBytes().length;
StringManager sm = StringManager.getManager(
ErrorReportValve.class.getPackage().getName(), Locale.ENGLISH);
String reason = sm.getString("http." + testData.getExpectedStatus() + ".reason");
int descriptionLength = sm.getString("http." + testData.getExpectedStatus() + ".desc")
.getBytes(StandardCharsets.UTF_8).length;
int statusHeaderLength = sm
.getString("errorReportValve.statusHeader",
String.valueOf(testData.getExpectedStatus()), reason)
.getBytes(StandardCharsets.UTF_8).length;
int typeLabelLength = sm.getString("errorReportValve.type")
.getBytes(StandardCharsets.UTF_8).length;
int statusReportLabelLength = sm.getString("errorReportValve.statusReport")
.getBytes(StandardCharsets.UTF_8).length;
int descriptionLabelLength = sm.getString("errorReportValve.description")
.getBytes(StandardCharsets.UTF_8).length;
// 196 bytes is the static length of the pure HTML code from the ErrorReportValve
int len = 196 + org.apache.catalina.util.TomcatCSS.TOMCAT_CSS
.getBytes(StandardCharsets.UTF_8).length +
typeLabelLength + statusReportLabelLength + descriptionLabelLength +
descriptionLength + serverInfoLength + statusHeaderLength * 2;
String contentLength = String.valueOf(len);
return getResponseBodyFrameTrace(streamId,
testData.getExpectedStatus(), "text/html;charset=utf-8",
"en", contentLength, contentLength);
} else {
Assert.fail();
// To keep the IDE happy
return null;
}
}
private static class TestData {
private final List<String> hostHeaders;
private final int expectedStatus;
public TestData(List<String> hostHeaders, int expectedStatus) {
this.hostHeaders = hostHeaders;
this.expectedStatus = expectedStatus;
}
public List<String> getHostHeaders() {
return hostHeaders;
}
public int getExpectedStatus() {
return expectedStatus;
}
}
}

View File

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

View File

@@ -0,0 +1,180 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.Test;
/**
* Unit tests for Section 3.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_3_2 extends Http2TestBase {
// Note: Tests for zero/multiple HTTP2-Settings fields can be found below
// in the tests for section 3.2.1
// TODO: Test initial requests with bodies of various sizes
@Test
public void testConnectionNoHttp2Support() throws Exception {
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2c", EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testConnectionUpgradeWrongProtocol() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(DEFAULT_CONNECTION_HEADER_VALUE, "h2", EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test(timeout=10000)
public void testConnectionNoPreface() throws Exception {
setupAsFarAsUpgrade();
// If we don't send the preface the server should kill the connection.
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test(timeout=10000)
public void testConnectionIncompletePrefaceStart() throws Exception {
setupAsFarAsUpgrade();
// If we send an incomplete preface the server should kill the
// connection.
os.write("PRI * HTTP/2.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
os.flush();
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test(timeout=10000)
public void testConnectionInvalidPrefaceStart() throws Exception {
setupAsFarAsUpgrade();
// If we send an incomplete preface the server should kill the
// connection.
os.write("xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxxx".getBytes(
StandardCharsets.ISO_8859_1));
os.flush();
try {
// Make the parser read something.
parser.readFrame(true);
} catch (IOException ioe) {
// Expected because the server is going to drop the connection.
}
}
@Test
public void testConnectionUpgradeFirstResponse() throws Exception{
super.http2Connect();
}
private void setupAsFarAsUpgrade() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
}
//------------------------------------------------------------ Section 3.2.1
@Test
public void testZeroHttp2Settings() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(Http2TestBase.DEFAULT_CONNECTION_HEADER_VALUE, "h2c", "", false);
parseHttp11Response();
}
@Test
public void testMultipleHttp2Settings() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade(Http2TestBase.DEFAULT_CONNECTION_HEADER_VALUE, "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER +
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testMissingConnectionValue() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("Upgrade", "h2c", Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, false);
parseHttp11Response();
}
@Test
public void testSplitConnectionValue01() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("Upgrade\r\nConnection: HTTP2-Settings", "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, true);
sendClientPreface();
validateHttp2InitialResponse();
}
@Test
public void testSplitConnectionValue02() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade("HTTP2-Settings\r\nConnection: Upgrade", "h2c",
Http2TestBase.EMPTY_HTTP2_SETTINGS_HEADER, true);
sendClientPreface();
validateHttp2InitialResponse();
}
// No need to test how trailing '=' are handled here. HTTP2Settings payloads
// are always a multiple of 6 long which means valid payloads never end in
// '='. Invalid payloads will be rejected anyway.
}

View File

@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import org.junit.Test;
public class TestHttp2Section_3_5 extends Http2TestBase {
@Test(expected=IOException.class)
public void testNoConnectionPreface() throws Exception {
enableHttp2();
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
// Should send client preface here
sendPing();
// Send several pings else server will block waiting for the client
// preface which is longer than a single ping.
sendPing();
sendPing();
validateHttp2InitialResponse();
}
}

View File

@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_1 extends Http2TestBase {
private static final byte[] UNKNOWN_FRAME = new byte[] {
0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00 };
// TODO: Tests for over-sized frames. Better located in tests for section 6?
@Test
public void testUnknownFrameType() throws Exception {
http2Connect();
os.write(UNKNOWN_FRAME);
os.flush();
sendSimpleGetRequest(3);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
// TODO: Tests for unexpected flags. Better located in tests for section 6?
@Test
public void testReservedBitIgnored() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Tweak the header to set the reserved bit
frameHeader[5] = (byte) (frameHeader[5] | 0x80);
// Process the request
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
}

View File

@@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_2 extends Http2TestBase {
@Test
public void testFrameSizeLimitsTooBig() throws Exception {
// HTTP2 upgrade
http2Connect();
// Overly large settings
// Settings have to be a multiple of six
int settingsCount = (ConnectionSettingsBase.DEFAULT_MAX_FRAME_SIZE / 6) + 1;
int size = settingsCount * 6;
byte[] settings = new byte[size + 9];
// Header
// Length
ByteUtil.setThreeBytes(settings, 0, size);
// Type
settings[3] = FrameType.SETTINGS.getIdByte();
// No flags
// Stream 0
// payload
for (int i = 0; i < settingsCount; i++) {
// Enable server push over and over again
ByteUtil.setTwoBytes(settings, (i * 6) + 9, 2);
ByteUtil.setFourBytes(settings, (i * 6) + 9 + 2, 1);
}
os.write(settings);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsTooBig() throws Exception {
// HTTP2 upgrade
http2Connect();
// Overly large ping
byte[] ping = new byte[109];
// Header
// Length
ByteUtil.setThreeBytes(ping, 0, 100);
// Type
ping[3] = FrameType.PING.getIdByte();
// No flags
// Stream 0
// Empty payload
os.write(ping);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsTooSmall() throws Exception {
// HTTP2 upgrade
http2Connect();
// Too small ping
byte[] ping = new byte[9];
// Header
// Length 0
// Type
ping[3] = FrameType.PING.getIdByte();
// No flags
// Stream 0
// Empty payload
os.write(ping);
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testFrameTypeLimitsStream() throws Exception {
// HTTP2 upgrade
http2Connect();
// Invalid priority
byte[] priority = new byte[9];
// Header
// Length 0
// Type
priority[3] = FrameType.PRIORITY.getIdByte();
// No flags
// Stream 3
ByteUtil.set31Bits(priority, 5, 3);
// Empty payload
os.write(priority);
// Read Stream reset frame
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().startsWith("3-RST-[6]"));
}
}

View File

@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 4.3 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_4_3 extends Http2TestBase {
@Test
public void testHeaderDecodingError() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Try and corrupt the headerPayload
headersPayload.put(0, (byte) (headersPayload.get(0) + 128));
// Process the request
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
@Test
public void testHeaderContinuationContiguous() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
// Part 2
headersPayload.clear();
buildSimpleGetRequestPart2(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
// headers, body
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testHeaderContinuationNonContiguous() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
sendPing();
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
}

View File

@@ -0,0 +1,288 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 5.§ of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_1 extends Http2TestBase {
@Test
public void testIdleStateInvalidFrame01() throws Exception {
http2Connect();
sendWindowUpdate(3, 200);
handleGoAwayResponse(1);
}
@Test
public void testIdleStateInvalidFrame02() throws Exception {
http2Connect();
sendData(3, new byte[] {});
handleGoAwayResponse(1);
}
// TODO: reserved local
// TODO: reserved remote
@Test
public void halfClosedRemoteInvalidFrame() throws Exception {
http2Connect();
// This half-closes the stream since it includes the end of stream flag
sendSimpleGetRequest(3);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
output.clearTrace();
// This should trigger a connection error
sendData(3, new byte[] {});
handleGoAwayResponse(3, Http2Error.STREAM_CLOSED);
}
@Test
public void testClosedInvalidFrame01() throws Exception {
// HTTP2 upgrade
http2Connect();
// Build the simple request
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
// Remove the end of stream and end of headers flags
frameHeader[4] = 0;
// Process the request
writeFrame(frameHeader, headersPayload);
// Send a rst
sendRst(3, Http2Error.INTERNAL_ERROR.getCode());
// Then try sending some data (which should fail)
sendData(3, new byte[] {});
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().startsWith("3-RST-[" + Http2Error.STREAM_CLOSED.getCode() + "]"));
}
@Test
public void testClosedInvalidFrame02() throws Exception {
http2Connect();
// Stream 1 is closed. This should trigger a connection error
sendData(1, new byte[] {});
handleGoAwayResponse(1, Http2Error.STREAM_CLOSED);
}
// TODO: Invalid frames for each of the remaining states
// Section 5.1.1
@Test
public void testClientSendEvenStream() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 4);
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1);
}
@Test
public void testClientSendOldStream() throws Exception {
http2Connect();
sendSimpleGetRequest(5);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace());
output.clearTrace();
// Build the simple request on an old stream
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequest(frameHeader, headersPayload, null, 3);
os.write(frameHeader);
os.flush();
handleGoAwayResponse(5);
}
@Test
public void testImplicitClose() throws Exception {
http2Connect();
sendPriority(3, 0, 16);
sendPriority(5, 0, 16);
sendSimpleGetRequest(5);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(5), output.getTrace());
output.clearTrace();
// Should trigger an error since stream 3 should have been implicitly
// closed.
sendSimpleGetRequest(3);
handleGoAwayResponse(5);
}
@Test
public void testExceedMaxActiveStreams() throws Exception {
// http2Connect() - modified
enableHttp2(1);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
// validateHttp2InitialResponse() - modified
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[1]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
getSimpleResponseTrace(1)
, output.getTrace());
output.clearTrace();
sendLargeGetRequest(3);
sendSimpleGetRequest(5);
// Default connection window size is 64k-1.
// Initial request will have used 8k leaving 56k-1.
// Stream window will be 64k-1.
// Expecting
// 1 * headers
// 56k-1 of body (7 * ~8k)
// 1 * error (could be in any order)
for (int i = 0; i < 8; i++) {
parser.readFrame(true);
}
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().contains("5-RST-[" +
Http2Error.REFUSED_STREAM.getCode() + "]"));
output.clearTrace();
// Connection window is zero.
// Stream window is 8k
// Release the remaining body
sendWindowUpdate(0, (1 << 31) - 2);
// Allow for the 8k still in the stream window
sendWindowUpdate(3, (1 << 31) - 8193);
// 192k of body (24 * 8k)
// 1 * error (could be in any order)
for (int i = 0; i < 24; i++) {
parser.readFrame(true);
}
}
@Test
public void testErrorOnWaitingStream() throws Exception {
// http2Connect() - modified
enableHttp2(1);
configureAndStartWebApplication();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
// validateHttp2InitialResponse() - modified
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[1]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
getSimpleResponseTrace(1)
, output.getTrace());
output.clearTrace();
sendLargeGetRequest(3);
sendSimpleGetRequest(5);
// Default connection window size is 64k-1.
// Initial request will have used 8k leaving 56k-1.
// Stream window will be 64k-1.
// Expecting
// 1 * headers
// 56k-1 of body (7 * ~8k)
// 1 * error (could be in any order)
for (int i = 0; i < 8; i++) {
parser.readFrame(true);
}
parser.readFrame(true);
Assert.assertTrue(output.getTrace(),
output.getTrace().contains("5-RST-[" +
Http2Error.REFUSED_STREAM.getCode() + "]"));
output.clearTrace();
// Connection window is zero.
// Stream window is 8k
// Expand the stream window too much to trigger an error
// Allow for the 8k still in the stream window
sendWindowUpdate(3, (1 << 31) - 1);
parser.readFrame(true);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Unit tests for Section 5.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_2 extends Http2TestBase {
/*
* Get the connection to a point where 1k of 8k response body has been
* read and the flow control for the stream has no capacity left.
*/
@Override
@Before
public void setUp() throws Exception {
super.setUp();
http2Connect();
// This test uses small window updates that will trigger the excessive
// overhead protection so disable it.
http2Protocol.setOverheadWindowUpdateThreshold(0);
// Set the default window size to 1024 bytes
sendSettings(0, false, new SettingValue(4, 1024));
// Wait for the ack
parser.readFrame(true);
output.clearTrace();
// Headers + 8k response
sendSimpleGetRequest(3);
// Headers
parser.readFrame(true);
// First 1k of body
parser.readFrame(true);
output.clearTrace();
}
@Test
public void testFlowControlLimits01() throws Exception {
readBytes(20);
clearRemainder();
}
@Test
public void testFlowControlLimits02() throws Exception {
readBytes(1);
readBytes(1);
readBytes(1024);
readBytes(1);
clearRemainder();
}
@Test
public void testFlowControlLimits03() throws Exception {
readBytes(8192,7168);
}
@Test
public void testFlowControlLimits04() throws Exception {
readBytes(7168, 7168, true);
}
private void readBytes(int len) throws Exception {
readBytes(len, len);
}
private void readBytes(int len, int expected) throws Exception {
readBytes(len, expected, len > expected);
}
private void readBytes(int len, int expected, boolean eos) throws Exception {
sendWindowUpdate(3, len);
parser.readFrame(true);
String expectedTrace = "3-Body-" + expected + "\n";
if (eos) {
expectedTrace += "3-EndOfStream\n";
}
Assert.assertEquals(expectedTrace, output.getTrace());
output.clearTrace();
}
private void clearRemainder() throws Exception {
// Remainder
sendWindowUpdate(3, 8192);
parser.readFrame(true);
}
}

View File

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

View File

@@ -0,0 +1,96 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 5.5 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_5_5 extends Http2TestBase {
private static final byte[] UNKNOWN_FRAME;
static {
// Unknown frame type
UNKNOWN_FRAME = new byte[29];
// Frame header
ByteUtil.setThreeBytes(UNKNOWN_FRAME, 0, 20);
// Type
UNKNOWN_FRAME[3] = (byte) 0x80;
// No flags
// Stream
ByteUtil.set31Bits(UNKNOWN_FRAME, 5, 5);
// zero payload
}
// Section 5.5
@Test
public void testUnknownSetting() throws Exception {
http2Connect();
// Unknown setting (should be ack'd)
sendSettings(0, false, new SettingValue(1 << 15, 0));
parser.readFrame(true);
Assert.assertEquals("0-Settings-Ack\n", output.getTrace());
}
@Test
public void testUnknownFrame() throws Exception {
http2Connect();
os.write(UNKNOWN_FRAME);
os.flush();
// Ping
sendPing();
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[0,0,0,0,0,0,0,0]\n", output.getTrace());
}
@Test
public void testNonContiguousHeaderWithUnknownFrame() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
os.write(UNKNOWN_FRAME);
os.flush();
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
}

View File

@@ -0,0 +1,162 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_1 extends Http2TestBase {
@Test
public void testDataFrame() throws Exception {
http2Connect();
sendSimplePostRequest(3, null);
readSimplePostResponse(false);
Assert.assertEquals("0-WindowSize-[128]\n" +
"3-WindowSize-[128]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[128]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-128\n" +
"3-EndOfStream\n", output.getTrace());
}
@Test
public void testDataFrameWithPadding() throws Exception {
http2Connect();
byte[] padding = new byte[8];
sendSimplePostRequest(3, padding);
readSimplePostResponse(true);
// The window update for the padding could occur anywhere since it
// happens on a different thead to the response.
String trace = output.getTrace();
String paddingWindowUpdate = "0-WindowSize-[9]\n3-WindowSize-[9]\n";
Assert.assertTrue(trace, trace.contains(paddingWindowUpdate));
trace = trace.replace(paddingWindowUpdate, "");
Assert.assertEquals("0-WindowSize-[119]\n" +
"3-WindowSize-[119]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[119]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-119\n" +
"3-EndOfStream\n", trace);
}
@Test
public void testDataFrameWithNonZeroPadding() throws Exception {
http2Connect();
byte[] padding = new byte[8];
padding[4] = 0x01;
sendSimplePostRequest(3, padding);
// May see Window updates depending on timing
skipWindowSizeFrames();
String trace = output.getTrace();
Assert.assertTrue(trace, trace.startsWith("0-Goaway-[3]-[1]-["));
}
@Test
public void testDataFrameOnStreamZero() throws Exception {
http2Connect();
byte[] dataFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(dataFrame, 0, 1);
// type (0 for data)
// flags (0)
// stream (0)
// payload (0)
os.write(dataFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testDataFrameTooMuchPadding() throws Exception {
http2Connect();
byte[] dataFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(dataFrame, 0, 1);
// type 0 (data)
// flags 8 (padded)
dataFrame[4] = 0x08;
// stream 3
ByteUtil.set31Bits(dataFrame, 5, 3);
// payload (pad length of 1)
dataFrame[9] = 1;
os.write(dataFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testDataFrameWithZeroLengthPadding() throws Exception {
http2Connect();
byte[] padding = new byte[0];
sendSimplePostRequest(3, padding);
// Since padding is zero length, response looks like there is none.
readSimplePostResponse(false);
Assert.assertEquals("0-WindowSize-[127]\n" +
"3-WindowSize-[127]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[127]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-127\n" +
"3-EndOfStream\n", output.getTrace());
}
}

View File

@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.2 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_2 extends Http2TestBase {
@Test
public void testHeaderFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 0);
writeFrame(frameHeader, headersPayload);
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameWithPadding() throws Exception {
http2Connect();
byte[] padding= new byte[8];
sendSimpleGetRequest(3, padding);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testHeaderFrameWithNonZeroPadding() throws Exception {
http2Connect();
byte[] padding= new byte[8];
padding[4] = 1;
sendSimpleGetRequest(3, padding);
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameTooMuchPadding() throws Exception {
http2Connect();
byte[] headerFrame = new byte[10];
// Header
// length
ByteUtil.setThreeBytes(headerFrame, 0, 1);
headerFrame[3] = FrameType.HEADERS.getIdByte();
// flags 8 (padded)
headerFrame[4] = 0x08;
// stream 3
ByteUtil.set31Bits(headerFrame, 5, 3);
// payload (pad length of 1)
headerFrame[9] = 1;
os.write(headerFrame);
os.flush();
handleGoAwayResponse(1);
}
@Test
public void testHeaderFrameWithZeroLengthPadding() throws Exception {
http2Connect();
byte[] padding= new byte[0];
sendSimpleGetRequest(3, padding);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
}

View File

@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.3 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_3 extends Http2TestBase {
@Test
public void testPriorityFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(0, 1, 15);
handleGoAwayResponse(1);
}
@Test
public void testPriorityFrameBetweenHeaderFrames() throws Exception {
// HTTP2 upgrade
http2Connect();
// Part 1
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(frameHeader, headersPayload, 3);
writeFrame(frameHeader, headersPayload);
sendPriority(5, 3, 15);
handleGoAwayResponse(1, Http2Error.COMPRESSION_ERROR);
}
@Test
public void testPriorityFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] priorityFrame = new byte[10];
// length
ByteUtil.setThreeBytes(priorityFrame, 0, 1);
// type
priorityFrame[3] = FrameType.PRIORITY.getIdByte();
// No flags
// Stream ID
ByteUtil.set31Bits(priorityFrame, 5, 3);
// Payload - left as zero
os.write(priorityFrame);
os.flush();
// Read reset frame
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n",
output.getTrace());
}
}

View File

@@ -0,0 +1,79 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.4 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_4 extends Http2TestBase {
@Test
public void testResetFrameOnStreamZero() throws Exception {
// HTTP2 upgrade
http2Connect();
sendRst(0, Http2Error.NO_ERROR.getCode());
handleGoAwayResponse(1);
}
@Test
public void testResetFrameOnIdleStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(3, 0, 15);
sendRst(3, Http2Error.NO_ERROR.getCode());
handleGoAwayResponse(1);
}
@Test
public void testResetFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] resetFrame = new byte[10];
// length
ByteUtil.setThreeBytes(resetFrame, 0, 1);
// type
resetFrame[3] = FrameType.RST.getIdByte();
// No flags
// Stream ID
ByteUtil.set31Bits(resetFrame, 5, 3);
// Payload - left as zero
os.write(resetFrame);
os.flush();
// Read reset frame
parser.readFrame(true);
Assert.assertEquals("3-RST-[" + Http2Error.FRAME_SIZE_ERROR.getCode() + "]\n",
output.getTrace());
}
}

View File

@@ -0,0 +1,127 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.5 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_5 extends Http2TestBase {
@Test
public void testSettingsFrameNonEmptAck() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, true, new SettingValue(1,1));
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
@Test
public void testSettingsFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPriority(3, 0, 15);
sendSettings(3, true, new SettingValue(1,1));
handleGoAwayResponse(1);
}
@Test
public void testSettingsFrameWrongLength() throws Exception {
// HTTP2 upgrade
http2Connect();
byte[] resetFrame = new byte[10];
// length
ByteUtil.setThreeBytes(resetFrame, 0, 1);
// type
resetFrame[3] = FrameType.SETTINGS.getIdByte();
// No flags
// Stream ID 0
// Payload - left as zero
os.write(resetFrame);
os.flush();
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
// Need to test sending push promise when push promise support is disabled
@Test
public void testSettingsFrameInvalidPushSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x2,0x2));
handleGoAwayResponse(1);
}
@Test
public void testSettingsFrameInvalidWindowSizeSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x4,1 << 31));
handleGoAwayResponse(1, Http2Error.FLOW_CONTROL_ERROR);
}
@Test
public void testSettingsFrameInvalidMaxFrameSizeSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0x5,1 << 31));
handleGoAwayResponse(1);
}
@Test
public void testSettingsUnknownSetting() throws Exception {
// HTTP2 upgrade
http2Connect();
sendSettings(0, false, new SettingValue(0xFF,0xFF));
// Ack
parser.readFrame(true);
Assert.assertTrue(output.getTrace(), output.getTrace().startsWith(
"0-Settings-Ack"));
}
// delayed ACKs. Requires an API (TBD) for applications to send settings.
}

View File

@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.charset.StandardCharsets;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.7 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_7 extends Http2TestBase {
@Test
public void testPingFrame() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, false, "01234567".getBytes(StandardCharsets.ISO_8859_1));
// Ping ack
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[48,49,50,51,52,53,54,55]\n", output.getTrace());
}
@Test
public void testPingFrameUnexpectedAck() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, true, "01234567".getBytes(StandardCharsets.ISO_8859_1));
sendPing(0, false, "76543210".getBytes(StandardCharsets.ISO_8859_1));
// Ping ack (only for second ping)
parser.readFrame(true);
Assert.assertEquals("0-Ping-Ack-[55,54,53,52,51,50,49,48]\n", output.getTrace());
}
@Test
public void testPingFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(1, false, "76543210".getBytes(StandardCharsets.ISO_8859_1));
handleGoAwayResponse(1);
}
@Test
public void testPingFrameWrongPayloadSize() throws Exception {
// HTTP2 upgrade
http2Connect();
sendPing(0, false, "6543210".getBytes(StandardCharsets.ISO_8859_1));
handleGoAwayResponse(1, Http2Error.FRAME_SIZE_ERROR);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 6.8 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* requirements in the RFC.
*/
public class TestHttp2Section_6_8 extends Http2TestBase {
private static final boolean RELAX_TIMING = Boolean.getBoolean("tomcat.test.relaxTiming");
private static final long PING_ACK_DELAY_MS = 2000;
// On slow systems (Gump) may need to be higher
private static final long TIMING_MARGIN_MS = RELAX_TIMING ? 1000 : 200;
@Test
public void testGoawayIgnoreNewStreams() throws Exception {
setPingAckDelayMillis(PING_ACK_DELAY_MS);
http2Connect();
http2Protocol.setMaxConcurrentStreams(200);
Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS);
getTomcatInstance().getConnector().pause();
// Go away
parser.readFrame(true);
Assert.assertEquals("0-Goaway-[2147483647]-[0]-[null]", output.getTrace());
output.clearTrace();
// Should be processed
sendSimpleGetRequest(3);
Thread.sleep(PING_ACK_DELAY_MS + TIMING_MARGIN_MS);
// Should be ignored
sendSimpleGetRequest(5);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
output.clearTrace();
// Finally the go away frame
parser.readFrame(true);
Assert.assertEquals("0-Goaway-[3]-[0]-[null]", output.getTrace());
}
@Test
public void testGoawayFrameNonZeroStream() throws Exception {
// HTTP2 upgrade
http2Connect();
sendGoaway(1, 1, Http2Error.NO_ERROR.getCode(), null);
handleGoAwayResponse(1);
}
// TODO Test header processing and window size processing for ignored
// streams
}

View File

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

View File

@@ -0,0 +1,217 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
/**
* Unit tests for Section 8.1 of
* <a href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>.
* <br>
* The order of tests in this class is aligned with the order of the
* examples in the RFC.
*/
public class TestHttp2Section_8_1 extends Http2TestBase {
@Test
public void testPostWithTrailerHeaders() throws Exception {
doTestPostWithTrailerHeaders(true);
}
@Test
public void testPostWithTrailerHeadersBlocked() throws Exception {
doTestPostWithTrailerHeaders(false);
}
private void doTestPostWithTrailerHeaders(boolean allowTrailerHeader) throws Exception{
http2Connect();
if (allowTrailerHeader) {
http2Protocol.setAllowedTrailerHeaders(TRAILER_HEADER_NAME);
}
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(256);
byte[] trailerFrameHeader = new byte[9];
ByteBuffer trailerPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, false, dataFrameHeader, dataPayload,
null, trailerFrameHeader, trailerPayload, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
// Body
writeFrame(dataFrameHeader, dataPayload);
// Trailers
writeFrame(trailerFrameHeader, trailerPayload);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
String len;
if (allowTrailerHeader) {
len = Integer.toString(256 + TRAILER_HEADER_VALUE.length());
} else {
len = "256";
}
Assert.assertEquals("0-WindowSize-[256]\n" +
"3-WindowSize-[256]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[" + len + "]\n" +
"3-Header-[date]-["+ DEFAULT_DATE + "]\n" +
"3-HeadersEnd\n" +
"3-Body-" +
len +
"\n" +
"3-EndOfStream\n",
output.getTrace());
}
@Test
public void testSendAck() throws Exception {
http2Connect();
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
byte[] dataFrameHeader = new byte[9];
ByteBuffer dataPayload = ByteBuffer.allocate(256);
buildPostRequest(headersFrameHeader, headersPayload, true,
dataFrameHeader, dataPayload, null, 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-HeadersStart\n" +
"3-Header-[:status]-[100]\n" +
"3-HeadersEnd\n",
output.getTrace());
output.clearTrace();
// Write the body
writeFrame(dataFrameHeader, dataPayload);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-WindowSize-[256]\n" +
"3-WindowSize-[256]\n" +
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-length]-[256]\n" +
"3-Header-[date]-["+ DEFAULT_DATE + "]\n" +
"3-HeadersEnd\n" +
"3-Body-256\n" +
"3-EndOfStream\n",
output.getTrace());
}
@Test
public void testUndefinedPseudoHeader() throws Exception {
List<Header> headers = new ArrayList<>(5);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header(":foo", "bar"));
doInvalidPseudoHeaderTest(headers);
}
@Test
public void testInvalidPseudoHeader() throws Exception {
List<Header> headers = new ArrayList<>(5);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header(":status", "200"));
doInvalidPseudoHeaderTest(headers);
}
@Test
public void testPseudoHeaderOrder() throws Exception {
// Need to do this in two frames because HPACK encoder automatically
// re-orders fields
http2Connect();
List<Header> headers = new ArrayList<>(4);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/simple"));
headers.add(new Header("x-test", "test"));
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildSimpleGetRequestPart1(headersFrameHeader, headersPayload, headers , 3);
writeFrame(headersFrameHeader, headersPayload);
headers.clear();
headers.add(new Header(":authority", "localhost:" + getPort()));
headersPayload.clear();
buildSimpleGetRequestPart2(headersFrameHeader, headersPayload, headers , 3);
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-RST-[1]\n", output.getTrace());
}
private void doInvalidPseudoHeaderTest(List<Header> headers) throws Exception {
http2Connect();
byte[] headersFrameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(headersFrameHeader, headersPayload, null, headers , 3);
// Write the headers
writeFrame(headersFrameHeader, headersPayload);
parser.readFrame(true);
Assert.assertEquals("3-RST-[1]\n", output.getTrace());
}
}

View File

@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class TestHttp2Timeouts extends Http2TestBase {
@Override
@Before
public void http2Connect() throws Exception {
super.http2Connect();
}
/*
* Simple request won't fill buffer so timeout will occur in Tomcat internal
* code during response completion.
*/
@Test
public void testClientWithEmptyWindow() throws Exception {
sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0));
sendSimpleGetRequest(3);
// Settings
parser.readFrame(false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Large request will fill buffer so timeout will occur in application code
* during response write (when Tomcat commits the response and flushes the
* buffer as a result of the buffer filling).
*/
@Test
public void testClientWithEmptyWindowLargeResponse() throws Exception {
sendSettings(0, false, new SettingValue(Setting.INITIAL_WINDOW_SIZE.getId(), 0));
sendLargeGetRequest(3);
// Settings
parser.readFrame(false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Timeout with app reading request body directly.
*/
@Test
public void testClientPostsNoBody() throws Exception {
sendSimplePostRequest(3, null, false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
/*
* Timeout with app processing parameters.
*/
@Test
public void testClientPostsNoParameters() throws Exception {
sendParameterPostRequest(3, null, null, 10, false);
// Headers
parser.readFrame(false);
output.clearTrace();
parser.readFrame(false);
Assert.assertEquals("3-RST-[11]\n", output.getTrace());
}
}

View File

@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public class TestHttp2UpgradeHandler extends Http2TestBase {
// https://bz.apache.org/bugzilla/show_bug.cgi?id=60970
@Test
public void testLargeHeader() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Tomcat.addServlet(ctxt, "large", new LargeHeaderServlet());
ctxt.addServletMappingDecoded("/large", "large");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/large");
writeFrame(frameHeader, headersPayload);
// Headers
parser.readFrame(true);
parser.readFrame(true);
// Body
parser.readFrame(true);
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[x-ignore]-[...]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
}

View File

@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
public class TestStream extends Http2TestBase {
/*
* https://bz.apache.org/bugzilla/show_bug.cgi?id=61120
*/
@Test
public void testPathParam() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Tomcat.addServlet(ctxt, "pathparam", new PathParam());
ctxt.addServletMappingDecoded("/pathparam", "pathparam");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3,
"/pathparam;jsessionid=" + PathParam.EXPECTED_SESSION_ID);
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
private static final class PathParam extends HttpServlet {
private static final long serialVersionUID = 1L;
public static final String EXPECTED_SESSION_ID = "0123456789ABCDEF";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
if (EXPECTED_SESSION_ID.equals(request.getRequestedSessionId())) {
response.getWriter().write("OK");
} else {
response.getWriter().write("FAIL");
}
}
}
}

View File

@@ -0,0 +1,226 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.compat.JrePlatform;
import org.apache.tomcat.util.http.FastHttpDateFormat;
public class TestStreamProcessor extends Http2TestBase {
@Test
public void testAsyncComplete() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
// Map the async servlet to /simple so we can re-use the HTTP/2 handling
// logic from the super class.
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncComplete());
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
// Flush before startAsync means body is written in two packets so an
// additional frame needs to be read
parser.readFrame(true);
Assert.assertEquals(
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-17\n" +
"3-Body-8\n" +
"3-EndOfStream\n", output.getTrace());
}
@Test
public void testAsyncDispatch() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
// Map the async servlet to /simple so we can re-use the HTTP/2 handling
// logic from the super class.
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
Wrapper w = Tomcat.addServlet(ctxt, "async", new AsyncDispatch());
w.setAsyncSupported(true);
ctxt.addServletMappingDecoded("/async", "async");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3, "/async");
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
}
@Test
public void testPrepareHeaders() throws Exception {
enableHttp2();
Tomcat tomcat = getTomcatInstance();
File appDir = new File("test/webapp");
Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
ctxt.addServletMappingDecoded("/simple", "simple");
tomcat.start();
openClientConnection();
doHttpUpgrade();
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
List<Header> headers = new ArrayList<>(3);
headers.add(new Header(":method", "GET"));
headers.add(new Header(":scheme", "http"));
headers.add(new Header(":path", "/index.html"));
headers.add(new Header(":authority", "localhost:" + getPort()));
headers.add(new Header("if-modified-since", FastHttpDateFormat.getCurrentDate()));
buildGetRequest(frameHeader, headersPayload, null, headers, 3);
writeFrame(frameHeader, headersPayload);
parser.readFrame(true);
StringBuilder expected = new StringBuilder();
expected.append("3-HeadersStart\n");
expected.append("3-Header-[:status]-[304]\n");
// Different line-endings -> different files size -> different weak eTag
if (JrePlatform.IS_WINDOWS) {
expected.append("3-Header-[etag]-[W/\"957-1447269522000\"]\n");
} else {
expected.append("3-Header-[etag]-[W/\"934-1447269522000\"]\n");
}
expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
expected.append("3-HeadersEnd\n");
Assert.assertEquals(expected.toString(), output.getTrace());
}
private static final class AsyncComplete extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
PrintWriter pw = response.getWriter();
pw.print("Enter-");
final AsyncContext asyncContext = request.startAsync(request, response);
pw.print("StartAsync-");
pw.flush();
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
asyncContext.getResponse().getWriter().print("Complete");
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
private static final class AsyncDispatch extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
asyncContext.dispatch("/simple");
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.coyote.http2;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.buf.HexUtils;
/*
* See https://bz.apache.org/bugzilla/show_bug.cgi?id=60482
*/
@RunWith(Parameterized.class)
public class TestStreamQueryString extends Http2TestBase {
@Parameters
public static Collection<Object[]> inputs() {
List<Object[]> result = new ArrayList<>();
// Test ASCII characters from 32 to 126 inclusive
for (int i = 32; i < 128; i++) {
result.add(new String[] { "%" + HexUtils.toHexString(new byte[] { (byte) i})});
}
return result;
}
private final String queryValueToTest;
public TestStreamQueryString(String queryValueToTest) {
this.queryValueToTest = queryValueToTest;
}
@Test
public void testQueryString() throws Exception {
String queryValue = "xxx" + queryValueToTest + "xxx";
enableHttp2();
Tomcat tomcat = getTomcatInstance();
Context ctxt = tomcat.addContext("", null);
Tomcat.addServlet(ctxt, "query", new Query(queryValue));
ctxt.addServletMappingDecoded("/query", "query");
tomcat.start();
openClientConnection();
doHttpUpgrade(queryValue);
sendClientPreface();
validateHttp2InitialResponse();
byte[] frameHeader = new byte[9];
ByteBuffer headersPayload = ByteBuffer.allocate(128);
buildGetRequest(frameHeader, headersPayload, null, 3,
"/query?" + Query.PARAM_NAME + "=" + queryValue);
writeFrame(frameHeader, headersPayload);
readSimpleGetResponse();
Assert.assertEquals(queryValue,
"3-HeadersStart\n" +
"3-Header-[:status]-[200]\n" +
"3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"3-Header-[content-length]-[2]\n" +
"3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"3-HeadersEnd\n" +
"3-Body-2\n" +
"3-EndOfStream\n", output.getTrace());
}
protected void doHttpUpgrade(String queryValue) throws IOException {
byte[] upgradeRequest = ("GET /query?" + Query.PARAM_NAME + "=" + queryValue + " HTTP/1.1\r\n" +
"Host: localhost:" + getPort() + "\r\n" +
"Connection: "+ DEFAULT_CONNECTION_HEADER_VALUE + "\r\n" +
"Upgrade: h2c\r\n" +
EMPTY_HTTP2_SETTINGS_HEADER +
"\r\n").getBytes(StandardCharsets.ISO_8859_1);
os.write(upgradeRequest);
os.flush();
Assert.assertTrue("Failed to read HTTP Upgrade response",
readHttpUpgradeResponse());
}
@Override
protected void validateHttp2InitialResponse() throws Exception {
// - 101 response acts as acknowledgement of the HTTP2-Settings header
// Need to read 5 frames
// - settings (server settings - must be first)
// - settings ack (for the settings frame in the client preface)
// - ping
// - headers (for response)
// - data (for response body)
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
parser.readFrame(true);
Assert.assertEquals("0-Settings-[3]-[200]\n" +
"0-Settings-End\n" +
"0-Settings-Ack\n" +
"0-Ping-[0,0,0,0,0,0,0,1]\n" +
"1-HeadersStart\n" +
"1-Header-[:status]-[200]\n" +
"1-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
"1-Header-[content-length]-[2]\n" +
"1-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
"1-HeadersEnd\n" +
"1-Body-2\n" +
"1-EndOfStream\n", output.getTrace());
output.clearTrace();
}
private static final class Query extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String PARAM_NAME = "param";
private final String expectedValue;
public Query(String expectedValue) {
String decoded;
try {
decoded = URLDecoder.decode(expectedValue, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Can't happen with UTF-8
decoded = null;
}
this.expectedValue = decoded;
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8");
if (expectedValue.equals(request.getParameter(PARAM_NAME))) {
response.getWriter().write("OK");
} else {
response.getWriter().write("FAIL");
}
}
}
}