View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.mylasta.direction.sponsor;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.Serializable;
22  import java.nio.charset.Charset;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.commons.fileupload2.core.DiskFileItem;
28  import org.apache.commons.fileupload2.core.DiskFileItemFactory;
29  import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
30  import org.apache.commons.fileupload2.core.FileUploadException;
31  import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
32  import org.apache.logging.log4j.LogManager;
33  import org.apache.logging.log4j.Logger;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.dbflute.helper.message.ExceptionMessageBuilder;
36  import org.lastaflute.core.message.UserMessages;
37  import org.lastaflute.web.exception.Forced404NotFoundException;
38  import org.lastaflute.web.ruts.multipart.MultipartFormFile;
39  import org.lastaflute.web.ruts.multipart.MultipartRequestHandler;
40  import org.lastaflute.web.ruts.multipart.MultipartRequestWrapper;
41  import org.lastaflute.web.ruts.multipart.exception.MultipartExceededException;
42  import org.lastaflute.web.util.LaServletContextUtil;
43  
44  import jakarta.servlet.ServletContext;
45  import jakarta.servlet.ServletException;
46  import jakarta.servlet.http.HttpServletRequest;
47  
48  /**
49   * The handler of multipart request (fileupload request). <br>
50   * This instance is created per one multipart request.
51   * @author modified by jflute (originated in Seasar)
52   */
53  public class FessMultipartRequestHandler implements MultipartRequestHandler {
54  
55      // ===================================================================================
56      //                                                                          Definition
57      //                                                                          ==========
58      private static final Logger logger = LogManager.getLogger(FessMultipartRequestHandler.class);
59  
60      // -----------------------------------------------------
61      //                                   Temporary Directory
62      //                                   -------------------
63      // used as repository for requested parameters
64      protected static final String CONTEXT_TEMPDIR_KEY = jakarta.servlet.ServletContext.TEMPDIR; // prior
65      protected static final String JAVA_IO_TMPDIR_KEY = "java.io.tmpdir"; // secondary
66  
67      // ===================================================================================
68      //                                                                           Attribute
69      //                                                                           =========
70      // keeping parsed request parameters, normal texts or uploaded files
71      // keys are requested parameter names (treated as field name here)
72      protected Map<String, Object> elementsAll; // lazy-loaded, then after not null
73      protected Map<String, MultipartFormFile> elementsFile; // me too
74      protected Map<String, String[]> elementsText; // me too
75  
76      // ===================================================================================
77      //                                                                      Handle Request
78      //                                                                      ==============
79      @Override
80      public void handleRequest(final HttpServletRequest request) throws ServletException {
81          final JakartaServletDiskFileUpload upload = createDiskFileUpload(request);
82          prepareElementsHash();
83          try {
84              final List<DiskFileItem> items = parseRequest(request, upload);
85              mappingParameter(request, items);
86          } catch (final FileUploadByteCountLimitException e) { // special handling
87              handleSizeLimitExceededException(request, e);
88          } catch (final FileUploadException e) { // contains fileCount exceeded
89              handleFileUploadException(e);
90          }
91      }
92  
93      protected void prepareElementsHash() { // traditional name
94          // #thinking jflute might lazy-loaded be unneeded? because created per request (2024/09/08)
95          elementsAll = new HashMap<>();
96          elementsText = new HashMap<>();
97          elementsFile = new HashMap<>();
98      }
99  
100     protected List<DiskFileItem> parseRequest(final HttpServletRequest request, final JakartaServletDiskFileUpload upload)
101             throws FileUploadException {
102         return upload.parseRequest(request);
103     }
104 
105     // ===================================================================================
106     //                                                                   ServletFileUpload
107     //                                                                   =================
108     protected JakartaServletDiskFileUpload createDiskFileUpload(final HttpServletRequest request) {
109         final DiskFileItemFactory fileItemFactory = createDiskFileItemFactory();
110         final JakartaServletDiskFileUpload upload = newServletFileUpload(fileItemFactory);
111         setupServletFileUpload(upload, request);
112         return upload;
113     }
114 
115     // -----------------------------------------------------
116     //                          DiskFileItemFactory Settings
117     //                          ----------------------------
118     protected DiskFileItemFactory createDiskFileItemFactory() {
119         final int sizeThreshold = getSizeThreshold();
120         final File repository = createRepositoryFile();
121         return DiskFileItemFactory.builder().setBufferSize(sizeThreshold).setFile(repository).get();
122     }
123 
124     protected int getSizeThreshold() {
125         return ComponentUtil.getFessConfig().getHttpFileuploadThresholdSizeAsInteger();
126     }
127 
128     protected File createRepositoryFile() {
129         return new File(getRepositoryPath());
130     }
131 
132     protected String getRepositoryPath() {
133         final ServletContext servletContext = LaServletContextUtil.getServletContext();
134         if (servletContext.getAttribute(CONTEXT_TEMPDIR_KEY) instanceof final File tempDirFile) {
135             final String tempDir = tempDirFile.getAbsolutePath();
136             if (tempDir != null && tempDir.length() > 0) {
137                 return tempDir;
138             }
139         }
140         return System.getProperty(JAVA_IO_TMPDIR_KEY);
141     }
142 
143     // -----------------------------------------------------
144     //                            ServletFileUpload Settings
145     //                            --------------------------
146     protected JakartaServletDiskFileUpload newServletFileUpload(final DiskFileItemFactory fileItemFactory) {
147         return new JakartaServletDiskFileUpload(fileItemFactory) {
148             @Override
149             public byte[] getBoundary(final String contentType) { // for security
150                 final byte[] boundary = super.getBoundary(contentType);
151                 checkBoundarySize(contentType, boundary);
152                 return boundary;
153             }
154         };
155     }
156 
157     // #for_now jflute to suppress CVE-2014-0050 even if commons-fileupload is older than safety version (2024/09/08)
158     // but if you use safety version, this extension is basically unneeded (or you can use it as double check)
159     protected void checkBoundarySize(final String contentType, final byte[] boundary) {
160         final int boundarySize = boundary.length;
161         final int limitSize = getBoundaryLimitSize();
162         if (boundarySize > getBoundaryLimitSize()) {
163             throwTooLongBoundarySizeException(contentType, boundarySize, limitSize);
164         }
165     }
166 
167     protected int getBoundaryLimitSize() {
168         // one HTTP proxy tool already limits the size (e.g. 3450 bytes)
169         // so specify this size for test
170         return 2000; // you can override as you like it
171     }
172 
173     protected void throwTooLongBoundarySizeException(final String contentType, final int boundarySize, final int limitSize) {
174         final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
175         br.addNotice("Too long boundary size so treats it as 404.");
176         br.addItem("Advice");
177         br.addElement("Against for CVE-2014-0050 (JVN14876762).");
178         br.addElement("Boundary size is limited by Framework.");
179         br.addElement("Too long boundary is treated as 404 because it's thought of as attack.");
180         br.addElement("");
181         br.addElement("While, you can override the boundary limit size");
182         br.addElement(" in " + getClass().getSimpleName() + ".");
183         br.addItem("Content Type");
184         br.addElement(contentType);
185         br.addItem("Boundary Size");
186         br.addElement(boundarySize);
187         br.addItem("Limit Size");
188         br.addElement(limitSize);
189         final String msg = br.buildExceptionMessage();
190         throw new Forced404NotFoundException(msg, UserMessages.empty()); // heavy attack!? so give no page to tell wasted action
191     }
192 
193     protected void setupServletFileUpload(final JakartaServletDiskFileUpload upload, final HttpServletRequest request) {
194         upload.setHeaderCharset(Charset.forName(request.getCharacterEncoding()));
195         upload.setMaxSize(getSizeMax());
196         upload.setMaxFileCount(getFileCountMax()); // since commons-fileupload-1.5
197     }
198 
199     protected long getSizeMax() {
200         return ComponentUtil.getFessConfig().getHttpFileuploadMaxSizeAsInteger().longValue();
201     }
202 
203     protected long getFileCountMax() {
204         return ComponentUtil.getFessConfig().getHttpFileuploadMaxFileCountAsInteger().longValue();
205     }
206 
207     // ===================================================================================
208     //                                                                   Parameter Mapping
209     //                                                                   =================
210     protected void mappingParameter(final HttpServletRequest request, final List<DiskFileItem> items) {
211         showFieldLoggingTitle();
212         for (DiskFileItem item : items) {
213             if (item.isFormField()) {
214                 showFormFieldParameter(item);
215                 addTextParameter(request, item);
216             } else {
217                 showFileFieldParameter(item);
218                 final String itemName = item.getName();
219                 if (itemName != null && !itemName.isEmpty()) {
220                     addFileParameter(item);
221                 }
222             }
223         }
224     }
225 
226     // -----------------------------------------------------
227     //                                     Parameter Logging
228     //                                     -----------------
229     // logging filter cannot show the parameters when multi-part so logging here
230     protected void showFieldLoggingTitle() {
231         if (logger.isDebugEnabled()) {
232             logger.debug("[Multipart Request Parameter]");
233         }
234     }
235 
236     protected void showFormFieldParameter(final DiskFileItem item) {
237         if (logger.isDebugEnabled()) {
238             try {
239                 logger.debug("[param] {}={}", item.getFieldName(), item.getString());
240             } catch (final IOException e) {
241                 logger.debug("[param] {}=(failed to read)", item.getFieldName());
242             }
243         }
244     }
245 
246     protected void showFileFieldParameter(final DiskFileItem item) {
247         if (logger.isDebugEnabled()) {
248             logger.debug("[param] {}:{name={}, size={}}", item.getFieldName(), item.getName(), item.getSize());
249         }
250     }
251 
252     // ===================================================================================
253     //                                                                       Add Parameter
254     //                                                                       =============
255     protected void addTextParameter(final HttpServletRequest request, final DiskFileItem item) {
256         final String fieldName = item.getFieldName();
257         final Charset encoding = Charset.forName(request.getCharacterEncoding());
258         String value = null;
259         boolean haveValue = false;
260         if (encoding != null) {
261             try {
262                 value = item.getString(encoding);
263                 haveValue = true;
264             } catch (final Exception e) {}
265         }
266         if (!haveValue) {
267             try {
268                 value = item.getString(Charset.forName("ISO-8859-1"));
269             } catch (final java.io.UnsupportedEncodingException uee) {
270                 try {
271                     value = item.getString();
272                 } catch (final IOException e) {
273                     throw new IllegalStateException("Failed to get string from the item: " + item, e);
274                 }
275             } catch (final IOException e) {
276                 throw new IllegalStateException("Failed to get string from the item: " + item, e);
277             }
278             haveValue = true;
279         }
280         if (request instanceof final MultipartRequestWrapper wrapper) {
281             wrapper.setParameter(fieldName, value);
282         }
283         final String[] oldArray = elementsText.get(fieldName);
284         final String[] newArray;
285         if (oldArray != null) {
286             newArray = new String[oldArray.length + 1];
287             System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
288             newArray[oldArray.length] = value;
289         } else {
290             newArray = new String[] { value };
291         }
292         elementsAll.put(fieldName, newArray);
293         elementsText.put(fieldName, newArray);
294     }
295 
296     protected void addFileParameter(final DiskFileItem item) {
297         final String fieldName = item.getFieldName();
298         final MultipartFormFile formFile = newActionMultipartFormFile(item);
299         elementsAll.put(fieldName, formFile);
300         elementsFile.put(fieldName, formFile);
301     }
302 
303     protected ActionMultipartFormFile newActionMultipartFormFile(final DiskFileItem item) {
304         return new ActionMultipartFormFile(item);
305     }
306 
307     // ===================================================================================
308     //                                                                  Exception Handling
309     //                                                                  ==================
310     protected void handleSizeLimitExceededException(final HttpServletRequest request, final FileUploadByteCountLimitException e) {
311         final long actual = e.getActualSize();
312         final long permitted = e.getPermitted();
313         final String msg = "Exceeded size of the multipart request: actual=" + actual + " permitted=" + permitted;
314         request.setAttribute(MAX_LENGTH_EXCEEDED_KEY, new MultipartExceededException(msg, actual, permitted, e));
315         try {
316             final InputStream is = request.getInputStream();
317             try {
318                 final byte[] buf = new byte[1024];
319                 while ((is.read(buf)) != -1) {}
320             } catch (final Exception ignored) {} finally {
321                 try {
322                     is.close();
323                 } catch (final Exception ignored) {}
324             }
325         } catch (final Exception ignored) {}
326     }
327 
328     protected void handleFileUploadException(final FileUploadException e) throws ServletException {
329         // suppress logging because it can be caught by logging filter
330         //log.error("Failed to parse multipart request", e);
331         throw new ServletException("Failed to upload the file.", e);
332     }
333 
334     // ===================================================================================
335     //                                                                           Roll-back
336     //                                                                           =========
337     @Override
338     public void rollback() {
339         for (MultipartFormFile formFile : elementsFile.values()) {
340             formFile.destroy();
341         }
342     }
343 
344     // ===================================================================================
345     //                                                                              Finish
346     //                                                                              ======
347     @Override
348     public void finish() {
349         rollback();
350     }
351 
352     // ===================================================================================
353     //                                                                           Form File
354     //                                                                           =========
355     protected static class ActionMultipartFormFile implements MultipartFormFile, Serializable {
356 
357         private static final long serialVersionUID = 1L;
358 
359         protected final DiskFileItem fileItem;
360 
361         public ActionMultipartFormFile(final DiskFileItem fileItem) {
362             this.fileItem = fileItem;
363         }
364 
365         @Override
366         public byte[] getFileData() throws IOException {
367             return fileItem.get();
368         }
369 
370         @Override
371         public InputStream getInputStream() throws IOException {
372             return fileItem.getInputStream();
373         }
374 
375         @Override
376         public String getContentType() {
377             return fileItem.getContentType();
378         }
379 
380         @Override
381         public int getFileSize() {
382             return (int) fileItem.getSize();
383         }
384 
385         @Override
386         public String getFileName() {
387             return getBaseFileName(fileItem.getName());
388         }
389 
390         protected String getBaseFileName(final String filePath) {
391             final String fileName = new File(filePath).getName();
392             int colonIndex = fileName.indexOf(":");
393             if (colonIndex == -1) {
394                 colonIndex = fileName.indexOf("\\\\"); // Windows SMB
395             }
396             final int backslashIndex = fileName.lastIndexOf("\\");
397             if (colonIndex > -1 && backslashIndex > -1) {
398                 return fileName.substring(backslashIndex + 1);
399             }
400             return fileName;
401         }
402 
403         @Override
404         public void destroy() {
405             try {
406                 fileItem.delete();
407             } catch (final IOException e) {
408                 throw new IllegalStateException("Failed to delete the fileItem: " + fileItem, e);
409             }
410         }
411 
412         @Override
413         public String toString() {
414             return "formFile:{" + getFileName() + "}";
415         }
416     }
417 
418     // ===================================================================================
419     //                                                                            Accessor
420     //                                                                            ========
421     @Override
422     public Map<String, Object> getAllElements() { // not null after parsing
423         return elementsAll;
424     }
425 
426     @Override
427     public Map<String, String[]> getTextElements() { // me too
428         return elementsText;
429     }
430 
431     @Override
432     public Map<String, MultipartFormFile> getFileElements() { // me too
433         return elementsFile;
434     }
435 }