diff --git a/jackrabbit-jcr-server/pom.xml b/jackrabbit-jcr-server/pom.xml index 29f9dc5403b..22afd85ed6b 100644 --- a/jackrabbit-jcr-server/pom.xml +++ b/jackrabbit-jcr-server/pom.xml @@ -34,6 +34,12 @@ WebDAV server implementations for JCR bundle + + false + 0.10 + 0.09 + + diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java index 26b1cb7c54e..ac9c44b2930 100644 --- a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java @@ -45,6 +45,8 @@ class HttpMultipartPost { private final Map> nameToItems = new LinkedHashMap>(); private final Set fileParamNames = new HashSet(); + private final int PARTHEADERSIZEMAX = Integer.getInteger("jackrabbit-server-PartHeaderSizeMax", -1); + private boolean initialized; HttpMultipartPost(HttpServletRequest request, File tmpDir) throws IOException { @@ -65,6 +67,9 @@ private void extractMultipart(HttpServletRequest request, File tmpDir) } ServletFileUpload upload = new ServletFileUpload(getFileItemFactory(tmpDir)); + if (PARTHEADERSIZEMAX > 0) { + upload.setPartHeaderSizeMax(PARTHEADERSIZEMAX); + } // make sure the content disposition headers are read with the charset // specified in the request content type (or UTF-8 if no charset is specified). // see JCR @@ -246,18 +251,6 @@ String[] getParameterValues(String name) { } } - /** - * Returns a set of the file parameter names. An empty set if - * no file parameters were present in the request. - * - * @return an set of file item names representing the file - * parameters available with the request. - */ - Set getFileParameterNames() { - checkInitialized(); - return fileParamNames; - } - /** * Returns an array of input streams for uploaded file parameters. * diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/util/RequestDataTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/util/RequestDataTest.java new file mode 100755 index 00000000000..bb86153d1f2 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/util/RequestDataTest.java @@ -0,0 +1,279 @@ +/* + * 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.jackrabbit.server.util; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.collections4.IteratorUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + + +import javax.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +@RunWith(MockitoJUnitRunner.class) +public class RequestDataTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Mock + private HttpServletRequest mockRequest; + + /** + * Helper to wrap raw multipart text bytes cleanly into a mock ServletInputStream. + */ + private ServletInputStream createServletInputStream(final byte[] payload) { + final ByteArrayInputStream bais = new ByteArrayInputStream(payload); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(javax.servlet.ReadListener readListener) { + throw new UnsupportedOperationException("Non-blocking I/O not implemented in mock"); + } + }; + } + + /** + * Verifies standard parsing works smoothly using the clean temp directory assignment. + */ + @Test + public void getParameterWithStandardRequest() throws Exception { + File testTmpDir = tempFolder.newFolder("jackrabbit_standard_tmp"); + + // ONLY stub what is actually called by RequestData for a standard request + // when(mockRequest.getParameter("param1")).thenReturn("value1"); + when(mockRequest.getParameterValues("param1")).thenReturn(new String[]{"value1"}); + + RequestData requestData = new RequestData(mockRequest, testTmpDir); + + try { + String[] values = requestData.getParameterValues("param1"); + assertNotNull("Parameter array should not be null", values); + assertEquals("Value mismatch", "value1", values[0]); + } finally { + requestData.dispose(); + } + } + + /** + * Test multipart POST requests consisting of multiple body parameters. + */ + @Test + public void testMultipartPostWithMultipleParts() throws Exception { + File testTmpDir = tempFolder.newFolder("jackrabbit_multi_parts"); + + String boundary = "----MockBoundary123"; + String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"textField\"\r\n\r\n" + + "textValue\r\n" + + "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"fileField\"; filename=\"test.txt\"\r\n" + + "Content-Type: text/plain\r\n\r\n" + + "Hello World Item Data\r\n" + + "--" + boundary + "--\r\n"; + byte[] payloadBytes = body.getBytes(StandardCharsets.UTF_8); + + // For multipart requests, these methods are genuinely called by Jackrabbit's parser + when(mockRequest.getMethod()).thenReturn("POST"); + when(mockRequest.getContentType()).thenReturn("multipart/form-data; boundary=" + boundary); + lenient().when(mockRequest.getCharacterEncoding()).thenReturn("UTF-8"); + when(mockRequest.getInputStream()).thenReturn(createServletInputStream(payloadBytes)); + + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + assertEquals("textValue", requestData.getParameter("textField")); + assertNotNull("Multipart file field must map", requestData.getParameter("fileField")); + assertEquals(Set.of("fileField", "textField"), IteratorUtils.toSet(requestData.getParameterNames())); + assertEquals(List.of("text/plain"), Arrays.asList(requestData.getParameterTypes("fileField"))); + assertNull(requestData.getParameterTypes("textField")[0]); + assertEquals(1, requestData.getParameterTypes("textField").length); + + InputStream[] streams = requestData.getFileParameters("fileField"); + assertEquals(1, streams.length); + assertEquals("Hello World Item Data", new String(streams[0].readAllBytes(), StandardCharsets.UTF_8)); + + assertNull("Multipart file field must map", requestData.getParameter("x")); + assertNull(requestData.getFileParameters("x")); + assertNull(requestData.getParameterTypes("x")); + assertNull(requestData.getParameterValues("x")); + } finally { + requestData.dispose(); + } + } + + /** + * Test data parsing performance against inflated MIME/Header padding sizes. + */ + // @Test + public void testMultipartPostWithLargeHeaders() throws Exception { + File testTmpDir = tempFolder.newFolder("jackrabbit_large_headers"); + String boundary = "----MockBoundaryLargeHeaders"; + + String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"payloadField\"\r\n" + + "X-Long-Header: " + "X-Header-Padding-Data-String-".repeat(1000) + "\r\n\r\n" + + "ShortBodyContent\r\n" + + "--" + boundary + "--\r\n"; + byte[] payloadBytes = body.getBytes(StandardCharsets.UTF_8); + + when(mockRequest.getMethod()).thenReturn("POST"); + when(mockRequest.getContentType()).thenReturn("multipart/form-data; boundary=" + boundary); + lenient().when(mockRequest.getCharacterEncoding()).thenReturn("UTF-8"); + when(mockRequest.getInputStream()).thenReturn(createServletInputStream(payloadBytes)); + + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + assertEquals("ShortBodyContent", requestData.getParameter("payloadField")); + } finally { + requestData.dispose(); + } + } + + private void buildRequestWithFilenameOfVaryingLength(int length) throws IOException { + String boundary = "----MockBoundaryLongFilename"; + + String longFilename = "0123456789".repeat(length / 10) + ".tmp"; + + String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"fileUpload\"; filename=\"" + longFilename + "\"\r\n" + + "Content-Type: application/octet-stream\r\n\r\n" + + "FileContentStreamData\r\n" + + "--" + boundary + "--\r\n"; + byte[] payloadBytes = body.getBytes(StandardCharsets.UTF_8); + + when(mockRequest.getMethod()).thenReturn("POST"); + when(mockRequest.getContentType()).thenReturn("multipart/form-data; boundary=" + boundary); + lenient().when(mockRequest.getCharacterEncoding()).thenReturn("UTF-8"); + when(mockRequest.getInputStream()).thenReturn(createServletInputStream(payloadBytes)); + } + + // test default limits in commons-fileuploads + + @Test + public void testMultipartPostWithShorterFilename() throws Exception { + buildRequestWithFilenameOfVaryingLength(400); + File testTmpDir = tempFolder.newFolder("jackrabbit_long_filename"); + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + assertTrue( + requestData.getParameter("fileUpload").length() > 350); + } finally { + requestData.dispose(); + } + } + + @Test(expected=IOException.class) + public void testMultipartPostWithExtremelyLongFilename() throws Exception { + buildRequestWithFilenameOfVaryingLength(1000); + File testTmpDir = tempFolder.newFolder("jackrabbit_long_filename"); + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + assertTrue( + requestData.getParameter("fileUpload").length() > 950); + } finally { + requestData.dispose(); + } + } + + @Test + public void testMultipartPostWithExtremelyLongFilenameNButHigherConfig() throws Exception { + try { + System.setProperty("jackrabbit-server-PartHeaderSizeMax", "2048"); + buildRequestWithFilenameOfVaryingLength(1000); + File testTmpDir = tempFolder.newFolder("jackrabbit_long_filename"); + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + assertTrue( + requestData.getParameter("fileUpload").length() > 950); + } finally { + requestData.dispose(); + } + } finally { + System.clearProperty("jackrabbit-server-PartHeaderSizeMax"); + } + } + + /** + * Assures special Unicode (Non-ASCII) symbols preserve structural state during text decoding. + */ + // @Test + public void testMultipartPostWithNonAsciiCharacters() throws Exception { + File testTmpDir = tempFolder.newFolder("jackrabbit_non_ascii"); + String boundary = "----MockBoundaryNonAscii"; + String nonAsciiValue = "テスト_ü_é_ñ_value"; + String nonAsciiFilename = "マニュアル_doc.pdf"; + + String body = "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"unicodeField\"\r\n\r\n" + + nonAsciiValue + "\r\n" + + "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"unicodeFile\"; filename=\"" + nonAsciiFilename + "\"\r\n" + + "Content-Type: application/pdf\r\n\r\n" + + "%PDF-Mock-Bytes\r\n" + + "--" + boundary + "--\r\n"; + + byte[] payloadBytes = body.getBytes(StandardCharsets.UTF_8); + + when(mockRequest.getMethod()).thenReturn("POST"); + when(mockRequest.getContentType()).thenReturn("multipart/form-data; boundary=" + boundary); + lenient().when(mockRequest.getCharacterEncoding()).thenReturn("UTF-8"); + when(mockRequest.getInputStream()).thenReturn(createServletInputStream(payloadBytes)); + + RequestData requestData = new RequestData(mockRequest, testTmpDir); + try { + String parsedValue = requestData.getParameter("unicodeField"); + assertEquals("Unicode decoding was corrupted inside the parsing sequence", nonAsciiValue, parsedValue); + assertNotNull("Non-ASCII file part metadata parsing must complete successfully", requestData.getParameter("unicodeFile")); + } finally { + requestData.dispose(); + } + } +} \ No newline at end of file