| /* |
| * 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.catalina.core; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.net.Socket; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.Collection; |
| |
| import javax.servlet.MultipartConfigElement; |
| import javax.servlet.ServletException; |
| import javax.servlet.annotation.MultipartConfig; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.Part; |
| |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| |
| 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; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| |
| public class TestSwallowAbortedUploads extends TomcatBaseTest { |
| |
| private static Log log = LogFactory.getLog(TestSwallowAbortedUploads.class); |
| |
| /* |
| * Test whether size limited uploads correctly handle connection draining. |
| */ |
| public Exception doAbortedUploadTest(AbortedUploadClient client, boolean limited, |
| boolean swallow) { |
| Exception ex = client.doRequest(limited, swallow); |
| if (log.isDebugEnabled()) { |
| log.debug("Response line: " + client.getResponseLine()); |
| log.debug("Response headers: " + client.getResponseHeaders()); |
| log.debug("Response body: " + client.getResponseBody()); |
| if (ex != null) { |
| log.debug("Exception in client: ", ex); |
| } |
| |
| } |
| return ex; |
| } |
| |
| /* |
| * Test whether aborted POST correctly handle connection draining. |
| */ |
| public Exception doAbortedPOSTTest(AbortedPOSTClient client, int status, |
| boolean swallow) { |
| Exception ex = client.doRequest(status, swallow); |
| if (log.isDebugEnabled()) { |
| log.debug("Response line: " + client.getResponseLine()); |
| log.debug("Response headers: " + client.getResponseHeaders()); |
| log.debug("Response body: " + client.getResponseBody()); |
| if (ex != null) { |
| log.info("Exception in client: ", ex); |
| } |
| |
| } |
| return ex; |
| } |
| |
| @Test |
| public void testAbortedUploadUnlimitedSwallow() { |
| log.info("Unlimited, swallow enabled"); |
| AbortedUploadClient client = new AbortedUploadClient(); |
| Exception ex = doAbortedUploadTest(client, false, true); |
| assertNull("Unlimited upload with swallow enabled generates client exception", |
| ex); |
| assertTrue("Unlimited upload with swallow enabled returns error status code", |
| client.isResponse200()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedUploadUnlimitedNoSwallow() { |
| log.info("Unlimited, swallow disabled"); |
| AbortedUploadClient client = new AbortedUploadClient(); |
| Exception ex = doAbortedUploadTest(client, false, false); |
| assertNull("Unlimited upload with swallow disabled generates client exception", |
| ex); |
| assertTrue("Unlimited upload with swallow disabled returns error status code", |
| client.isResponse200()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedUploadLimitedSwallow() { |
| log.info("Limited, swallow enabled"); |
| AbortedUploadClient client = new AbortedUploadClient(); |
| Exception ex = doAbortedUploadTest(client, true, true); |
| assertNull("Limited upload with swallow enabled generates client exception", |
| ex); |
| assertTrue("Limited upload with swallow enabled returns non-500 status code", |
| client.isResponse500()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedUploadLimitedNoSwallow() { |
| log.info("Limited, swallow disabled"); |
| AbortedUploadClient client = new AbortedUploadClient(); |
| Exception ex = doAbortedUploadTest(client, true, false); |
| assertTrue("Limited upload with swallow disabled does not generate client exception", |
| ex != null && ex instanceof java.net.SocketException); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedPOSTOKSwallow() { |
| log.info("Aborted (OK), swallow enabled"); |
| AbortedPOSTClient client = new AbortedPOSTClient(); |
| Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_OK, true); |
| assertNull("Unlimited upload with swallow enabled generates client exception", |
| ex); |
| assertTrue("Unlimited upload with swallow enabled returns error status code", |
| client.isResponse200()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedPOSTOKNoSwallow() { |
| log.info("Aborted (OK), swallow disabled"); |
| AbortedPOSTClient client = new AbortedPOSTClient(); |
| Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_OK, false); |
| assertNull("Unlimited upload with swallow disabled generates client exception", |
| ex); |
| assertTrue("Unlimited upload with swallow disabled returns error status code", |
| client.isResponse200()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedPOST413Swallow() { |
| log.info("Aborted (413), swallow enabled"); |
| AbortedPOSTClient client = new AbortedPOSTClient(); |
| Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, true); |
| assertNull("Limited upload with swallow enabled generates client exception", |
| ex); |
| assertTrue("Limited upload with swallow enabled returns error status code", |
| client.isResponse413()); |
| client.reset(); |
| } |
| |
| @Test |
| public void testAbortedPOST413NoSwallow() { |
| log.info("Aborted (413), swallow disabled"); |
| AbortedPOSTClient client = new AbortedPOSTClient(); |
| Exception ex = doAbortedPOSTTest(client, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, false); |
| assertTrue("Limited upload with swallow disabled does not generate client exception", |
| ex != null && ex instanceof java.net.SocketException); |
| client.reset(); |
| } |
| |
| @MultipartConfig |
| private static class AbortedUploadServlet extends HttpServlet { |
| |
| private static final long serialVersionUID = 1L; |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) |
| throws ServletException, IOException { |
| PrintWriter out = resp.getWriter(); |
| resp.setContentType("text/plain"); |
| resp.setCharacterEncoding("UTF-8"); |
| StringBuilder sb = new StringBuilder(); |
| try { |
| Collection<Part> c = req.getParts(); |
| if (c == null) { |
| log.debug("Count: -1"); |
| sb.append("Count: -1\n"); |
| } else { |
| log.debug("Count: " + c.size()); |
| sb.append("Count: " + c.size() + "\n"); |
| for (Part p : c) { |
| log.debug("Name: " + p.getName() + ", Size: " |
| + p.getSize()); |
| sb.append("Name: " + p.getName() + ", Size: " |
| + p.getSize() + "\n"); |
| } |
| } |
| } catch (IllegalStateException ex) { |
| log.debug("IllegalStateException during getParts()"); |
| sb.append("IllegalStateException during getParts()\n"); |
| resp.setStatus(500); |
| } catch (Throwable ex) { |
| log.error("Exception during getParts()", ex); |
| sb.append(ex); |
| resp.setStatus(500); |
| } |
| out.print(sb.toString()); |
| resp.flushBuffer(); |
| } |
| |
| } |
| |
| /** |
| * Test no connection draining when upload too large |
| */ |
| private class AbortedUploadClient extends SimpleHttpClient { |
| |
| private static final String URI = "/uploadAborted"; |
| private static final String servletName = "uploadAborted"; |
| private static final int limitSize = 100; |
| private static final int hugeSize = 2000000; |
| |
| private Context context; |
| |
| private synchronized void init(boolean limited, boolean swallow) |
| throws Exception { |
| |
| Tomcat tomcat = getTomcatInstance(); |
| context = tomcat.addContext("", TEMP_DIR); |
| Wrapper w; |
| w = Tomcat.addServlet(context, servletName, |
| new AbortedUploadServlet()); |
| // Tomcat.addServlet does not respect annotations, so we have |
| // to set our own MultipartConfigElement. |
| // Choose upload file size limit. |
| if (limited) { |
| w.setMultipartConfigElement(new MultipartConfigElement("", |
| limitSize, -1, -1)); |
| } else { |
| w.setMultipartConfigElement(new MultipartConfigElement("")); |
| } |
| context.addServletMapping(URI, servletName); |
| context.setSwallowAbortedUploads(swallow); |
| |
| tomcat.start(); |
| setPort(tomcat.getConnector().getLocalPort()); |
| } |
| |
| private Exception doRequest(boolean limited, boolean swallow) { |
| char body[] = new char[hugeSize]; |
| Arrays.fill(body, 'X'); |
| |
| try { |
| init(limited, swallow); |
| |
| // Open connection |
| connect(); |
| |
| // Send specified request body using method |
| String[] request; |
| |
| String boundary = "--simpleboundary"; |
| StringBuilder sb = new StringBuilder(); |
| |
| sb.append("--"); |
| sb.append(boundary); |
| sb.append(CRLF); |
| sb.append("Content-Disposition: form-data; name=\"part\""); |
| sb.append(CRLF); |
| sb.append(CRLF); |
| sb.append(body); |
| sb.append(CRLF); |
| sb.append("--"); |
| sb.append(boundary); |
| sb.append("--"); |
| sb.append(CRLF); |
| |
| // Re-encode the content so that bytes = characters |
| String content = new String(sb.toString().getBytes("UTF-8"), |
| "ASCII"); |
| |
| request = new String[] { "POST http://localhost:" + getPort() + URI + " HTTP/1.1" + CRLF |
| + "Host: localhost" + CRLF |
| + "Connection: close" + CRLF |
| + "Content-Type: multipart/form-data; boundary=" + boundary + CRLF |
| + "Content-Length: " + content.length() + CRLF |
| + CRLF |
| + content + 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() { |
| return false; // Don't care |
| } |
| } |
| |
| private static class AbortedPOSTServlet extends HttpServlet { |
| |
| private static final long serialVersionUID = 1L; |
| |
| private int status = 200; |
| |
| public void setStatus(int status) { |
| this.status = status; |
| } |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) |
| throws ServletException, IOException { |
| resp.setContentType("text/plain"); |
| resp.setCharacterEncoding("UTF-8"); |
| resp.setStatus(status); |
| PrintWriter out = resp.getWriter(); |
| out.print("OK"); |
| resp.flushBuffer(); |
| } |
| |
| } |
| |
| /** |
| * Test no connection draining when upload too large |
| */ |
| private class AbortedPOSTClient extends SimpleHttpClient { |
| |
| private static final String URI = "/uploadAborted"; |
| private static final String servletName = "uploadAborted"; |
| private static final int hugeSize = 2000000; |
| |
| private Context context; |
| |
| private synchronized void init(int status, boolean swallow) |
| throws Exception { |
| |
| Tomcat tomcat = getTomcatInstance(); |
| context = tomcat.addContext("", TEMP_DIR); |
| AbortedPOSTServlet servlet = new AbortedPOSTServlet(); |
| servlet.setStatus(status); |
| Tomcat.addServlet(context, servletName, |
| servlet); |
| context.addServletMapping(URI, servletName); |
| context.setSwallowAbortedUploads(swallow); |
| |
| tomcat.start(); |
| |
| setPort(tomcat.getConnector().getLocalPort()); |
| } |
| |
| private Exception doRequest(int status, boolean swallow) { |
| char body[] = new char[hugeSize]; |
| Arrays.fill(body, 'X'); |
| |
| try { |
| init(status, swallow); |
| |
| // Open connection |
| connect(); |
| |
| // Send specified request body using method |
| String[] request; |
| |
| String content = new String(body); |
| |
| request = new String[] { "POST http://localhost:" + getPort() + URI + " HTTP/1.1" + CRLF |
| + "Host: localhost" + CRLF |
| + "Connection: close" + CRLF |
| + "Content-Length: " + content.length() + CRLF |
| + CRLF |
| + content + 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() { |
| return false; // Don't care |
| } |
| } |
| |
| |
| @Test |
| public void testChunkedPUTLimit() throws Exception { |
| doTestChunkedPUT(true); |
| } |
| |
| |
| @Test |
| public void testChunkedPUTNoLimit() throws Exception { |
| doTestChunkedPUT(false); |
| } |
| |
| |
| public void doTestChunkedPUT(boolean limit) throws Exception { |
| |
| Tomcat tomcat = getTomcatInstance(); |
| tomcat.addContext("", TEMP_DIR); |
| // No need for target to exist. |
| |
| if (!limit) { |
| tomcat.getConnector().setAttribute("maxSwallowSize", "-1"); |
| } |
| |
| tomcat.start(); |
| |
| Exception writeEx = null; |
| Exception readEx = null; |
| String responseLine = null; |
| |
| try (Socket conn = new Socket("localhost", getPort())) { |
| Writer writer = new OutputStreamWriter( |
| conn.getOutputStream(), StandardCharsets.US_ASCII); |
| writer.write("PUT /does-not-exist HTTP/1.1\r\n"); |
| writer.write("Host: any\r\n"); |
| writer.write("Transfer-encoding: chunked\r\n"); |
| writer.write("\r\n"); |
| |
| // Smarter than the typical client. Attempts to read the response |
| // even if the request is not fully written. |
| try { |
| // Write (or try to write) 16MB |
| for (int i = 0; i < 1024 * 1024; i++) { |
| writer.write("10\r\n"); |
| writer.write("0123456789ABCDEF\r\n"); |
| } |
| } catch (Exception e) { |
| writeEx = e; |
| } |
| |
| try { |
| BufferedReader reader = new BufferedReader(new InputStreamReader( |
| conn.getInputStream(), StandardCharsets.US_ASCII)); |
| |
| responseLine = reader.readLine(); |
| } catch (IOException e) { |
| readEx = e; |
| } |
| } |
| |
| if (limit) { |
| Assert.assertNotNull(writeEx); |
| } else { |
| Assert.assertNull(writeEx); |
| Assert.assertNull(readEx); |
| Assert.assertNotNull(responseLine); |
| Assert.assertTrue(responseLine.contains("404")); |
| } |
| } |
| } |