1+ /*
2+ * Licensed to the Apache Software Foundation (ASF) under one or more
3+ * contributor license agreements. See the NOTICE file distributed with
4+ * this work for additional information regarding copyright ownership.
5+ * The ASF licenses this file to You under the Apache License, Version 2.0
6+ * (the "License"); you may not use this file except in compliance with
7+ * the License. You may obtain a copy of the License at
8+ *
9+ * http://www.apache.org/licenses/LICENSE-2.0
10+ *
11+ * Unless required by applicable law or agreed to in writing, software
12+ * distributed under the License is distributed on an "AS IS" BASIS,
13+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ * See the License for the specific language governing permissions and
15+ * limitations under the License.
16+ */
17+ package org .apache .jackrabbit .server .util ;
18+
19+ import javax .servlet .http .HttpServletRequest ;
20+
21+ import org .apache .commons .collections4 .IteratorUtils ;
22+ import org .junit .Rule ;
23+ import org .junit .Test ;
24+ import org .junit .rules .TemporaryFolder ;
25+ import org .junit .runner .RunWith ;
26+ import org .mockito .Mock ;
27+ import org .mockito .junit .MockitoJUnitRunner ;
28+
29+ import java .io .File ;
30+
31+ import static org .junit .Assert .assertEquals ;
32+ import static org .junit .Assert .assertNotNull ;
33+ import static org .junit .Assert .assertNull ;
34+ import static org .junit .Assert .assertTrue ;
35+ import static org .mockito .Mockito .*;
36+
37+
38+ import javax .servlet .ServletInputStream ;
39+ import java .io .ByteArrayInputStream ;
40+ import java .io .IOException ;
41+ import java .io .InputStream ;
42+ import java .nio .charset .StandardCharsets ;
43+ import java .util .Arrays ;
44+ import java .util .List ;
45+ import java .util .Set ;
46+
47+ @ RunWith (MockitoJUnitRunner .class )
48+ public class RequestDataTest {
49+
50+ @ Rule
51+ public TemporaryFolder tempFolder = new TemporaryFolder ();
52+
53+ @ Mock
54+ private HttpServletRequest mockRequest ;
55+
56+ /**
57+ * Helper to wrap raw multipart text bytes cleanly into a mock ServletInputStream.
58+ */
59+ private ServletInputStream createServletInputStream (final byte [] payload ) {
60+ final ByteArrayInputStream bais = new ByteArrayInputStream (payload );
61+ return new ServletInputStream () {
62+ @ Override
63+ public int read () {
64+ return bais .read ();
65+ }
66+
67+ @ Override
68+ public boolean isFinished () {
69+ return bais .available () == 0 ;
70+ }
71+
72+ @ Override
73+ public boolean isReady () {
74+ return true ;
75+ }
76+
77+ @ Override
78+ public void setReadListener (javax .servlet .ReadListener readListener ) {
79+ throw new UnsupportedOperationException ("Non-blocking I/O not implemented in mock" );
80+ }
81+ };
82+ }
83+
84+ /**
85+ * Verifies standard parsing works smoothly using the clean temp directory assignment.
86+ */
87+ @ Test
88+ public void getParameterWithStandardRequest () throws Exception {
89+ File testTmpDir = tempFolder .newFolder ("jackrabbit_standard_tmp" );
90+
91+ // ONLY stub what is actually called by RequestData for a standard request
92+ // when(mockRequest.getParameter("param1")).thenReturn("value1");
93+ when (mockRequest .getParameterValues ("param1" )).thenReturn (new String []{"value1" });
94+
95+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
96+
97+ try {
98+ String [] values = requestData .getParameterValues ("param1" );
99+ assertNotNull ("Parameter array should not be null" , values );
100+ assertEquals ("Value mismatch" , "value1" , values [0 ]);
101+ } finally {
102+ requestData .dispose ();
103+ }
104+ }
105+
106+ /**
107+ * Test multipart POST requests consisting of multiple body parameters.
108+ */
109+ @ Test
110+ public void testMultipartPostWithMultipleParts () throws Exception {
111+ File testTmpDir = tempFolder .newFolder ("jackrabbit_multi_parts" );
112+
113+ String boundary = "----MockBoundary123" ;
114+ String body = "--" + boundary + "\r \n " +
115+ "Content-Disposition: form-data; name=\" textField\" \r \n \r \n " +
116+ "textValue\r \n " +
117+ "--" + boundary + "\r \n " +
118+ "Content-Disposition: form-data; name=\" fileField\" ; filename=\" test.txt\" \r \n " +
119+ "Content-Type: text/plain\r \n \r \n " +
120+ "Hello World Item Data\r \n " +
121+ "--" + boundary + "--\r \n " ;
122+ byte [] payloadBytes = body .getBytes (StandardCharsets .UTF_8 );
123+
124+ // For multipart requests, these methods are genuinely called by Jackrabbit's parser
125+ when (mockRequest .getMethod ()).thenReturn ("POST" );
126+ when (mockRequest .getContentType ()).thenReturn ("multipart/form-data; boundary=" + boundary );
127+ lenient ().when (mockRequest .getCharacterEncoding ()).thenReturn ("UTF-8" );
128+ when (mockRequest .getInputStream ()).thenReturn (createServletInputStream (payloadBytes ));
129+
130+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
131+ try {
132+ assertEquals ("textValue" , requestData .getParameter ("textField" ));
133+ assertNotNull ("Multipart file field must map" , requestData .getParameter ("fileField" ));
134+ assertEquals (Set .of ("fileField" , "textField" ), IteratorUtils .toSet (requestData .getParameterNames ()));
135+ assertEquals (List .of ("text/plain" ), Arrays .asList (requestData .getParameterTypes ("fileField" )));
136+ assertNull (requestData .getParameterTypes ("textField" )[0 ]);
137+ assertEquals (1 , requestData .getParameterTypes ("textField" ).length );
138+
139+ InputStream [] streams = requestData .getFileParameters ("fileField" );
140+ assertEquals (1 , streams .length );
141+ assertEquals ("Hello World Item Data" , new String (streams [0 ].readAllBytes (), StandardCharsets .UTF_8 ));
142+
143+ assertNull ("Multipart file field must map" , requestData .getParameter ("x" ));
144+ assertNull (requestData .getFileParameters ("x" ));
145+ assertNull (requestData .getParameterTypes ("x" ));
146+ assertNull (requestData .getParameterValues ("x" ));
147+ } finally {
148+ requestData .dispose ();
149+ }
150+ }
151+
152+ /**
153+ * Test data parsing performance against inflated MIME/Header padding sizes.
154+ */
155+ // @Test
156+ public void testMultipartPostWithLargeHeaders () throws Exception {
157+ File testTmpDir = tempFolder .newFolder ("jackrabbit_large_headers" );
158+ String boundary = "----MockBoundaryLargeHeaders" ;
159+
160+ String body = "--" + boundary + "\r \n " +
161+ "Content-Disposition: form-data; name=\" payloadField\" \r \n " +
162+ "X-Long-Header: " + "X-Header-Padding-Data-String-" .repeat (1000 ) + "\r \n \r \n " +
163+ "ShortBodyContent\r \n " +
164+ "--" + boundary + "--\r \n " ;
165+ byte [] payloadBytes = body .getBytes (StandardCharsets .UTF_8 );
166+
167+ when (mockRequest .getMethod ()).thenReturn ("POST" );
168+ when (mockRequest .getContentType ()).thenReturn ("multipart/form-data; boundary=" + boundary );
169+ lenient ().when (mockRequest .getCharacterEncoding ()).thenReturn ("UTF-8" );
170+ when (mockRequest .getInputStream ()).thenReturn (createServletInputStream (payloadBytes ));
171+
172+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
173+ try {
174+ assertEquals ("ShortBodyContent" , requestData .getParameter ("payloadField" ));
175+ } finally {
176+ requestData .dispose ();
177+ }
178+ }
179+
180+ private void buildRequestWithFilenameOfVaryingLength (int length ) throws IOException {
181+ String boundary = "----MockBoundaryLongFilename" ;
182+
183+ String longFilename = "0123456789" .repeat (length / 10 ) + ".tmp" ;
184+
185+ String body = "--" + boundary + "\r \n " +
186+ "Content-Disposition: form-data; name=\" fileUpload\" ; filename=\" " + longFilename + "\" \r \n " +
187+ "Content-Type: application/octet-stream\r \n \r \n " +
188+ "FileContentStreamData\r \n " +
189+ "--" + boundary + "--\r \n " ;
190+ byte [] payloadBytes = body .getBytes (StandardCharsets .UTF_8 );
191+
192+ when (mockRequest .getMethod ()).thenReturn ("POST" );
193+ when (mockRequest .getContentType ()).thenReturn ("multipart/form-data; boundary=" + boundary );
194+ lenient ().when (mockRequest .getCharacterEncoding ()).thenReturn ("UTF-8" );
195+ when (mockRequest .getInputStream ()).thenReturn (createServletInputStream (payloadBytes ));
196+ }
197+
198+ // test default limits in commons-fileuploads
199+
200+ @ Test
201+ public void testMultipartPostWithShorterFilename () throws Exception {
202+ buildRequestWithFilenameOfVaryingLength (400 );
203+ File testTmpDir = tempFolder .newFolder ("jackrabbit_long_filename" );
204+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
205+ try {
206+ assertTrue (
207+ requestData .getParameter ("fileUpload" ).length () > 350 );
208+ } finally {
209+ requestData .dispose ();
210+ }
211+ }
212+
213+ @ Test (expected =IOException .class )
214+ public void testMultipartPostWithExtremelyLongFilename () throws Exception {
215+ buildRequestWithFilenameOfVaryingLength (1000 );
216+ File testTmpDir = tempFolder .newFolder ("jackrabbit_long_filename" );
217+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
218+ try {
219+ assertTrue (
220+ requestData .getParameter ("fileUpload" ).length () > 950 );
221+ } finally {
222+ requestData .dispose ();
223+ }
224+ }
225+
226+ @ Test
227+ public void testMultipartPostWithExtremelyLongFilenameNButHigherConfig () throws Exception {
228+ try {
229+ System .setProperty ("jackrabbit-server-PartHeaderSizeMax" , "2048" );
230+ buildRequestWithFilenameOfVaryingLength (1000 );
231+ File testTmpDir = tempFolder .newFolder ("jackrabbit_long_filename" );
232+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
233+ try {
234+ assertTrue (
235+ requestData .getParameter ("fileUpload" ).length () > 950 );
236+ } finally {
237+ requestData .dispose ();
238+ }
239+ } finally {
240+ System .clearProperty ("jackrabbit-server-PartHeaderSizeMax" );
241+ }
242+ }
243+
244+ /**
245+ * Assures special Unicode (Non-ASCII) symbols preserve structural state during text decoding.
246+ */
247+ // @Test
248+ public void testMultipartPostWithNonAsciiCharacters () throws Exception {
249+ File testTmpDir = tempFolder .newFolder ("jackrabbit_non_ascii" );
250+ String boundary = "----MockBoundaryNonAscii" ;
251+ String nonAsciiValue = "テスト_ü_é_ñ_value" ;
252+ String nonAsciiFilename = "マニュアル_doc.pdf" ;
253+
254+ String body = "--" + boundary + "\r \n " +
255+ "Content-Disposition: form-data; name=\" unicodeField\" \r \n \r \n " +
256+ nonAsciiValue + "\r \n " +
257+ "--" + boundary + "\r \n " +
258+ "Content-Disposition: form-data; name=\" unicodeFile\" ; filename=\" " + nonAsciiFilename + "\" \r \n " +
259+ "Content-Type: application/pdf\r \n \r \n " +
260+ "%PDF-Mock-Bytes\r \n " +
261+ "--" + boundary + "--\r \n " ;
262+
263+ byte [] payloadBytes = body .getBytes (StandardCharsets .UTF_8 );
264+
265+ when (mockRequest .getMethod ()).thenReturn ("POST" );
266+ when (mockRequest .getContentType ()).thenReturn ("multipart/form-data; boundary=" + boundary );
267+ lenient ().when (mockRequest .getCharacterEncoding ()).thenReturn ("UTF-8" );
268+ when (mockRequest .getInputStream ()).thenReturn (createServletInputStream (payloadBytes ));
269+
270+ RequestData requestData = new RequestData (mockRequest , testTmpDir );
271+ try {
272+ String parsedValue = requestData .getParameter ("unicodeField" );
273+ assertEquals ("Unicode decoding was corrupted inside the parsing sequence" , nonAsciiValue , parsedValue );
274+ assertNotNull ("Non-ASCII file part metadata parsing must complete successfully" , requestData .getParameter ("unicodeFile" ));
275+ } finally {
276+ requestData .dispose ();
277+ }
278+ }
279+ }
0 commit comments