Java tutorial
/* * 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.struts.extras; import com.opensymphony.xwork2.LocaleProvider; import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.util.LocalizedTextUtil; import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; import org.apache.struts2.StrutsConstants; import org.apache.struts2.dispatcher.multipart.MultiPartRequest; import javax.servlet.http.HttpServletRequest; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; public class SecureJakartaStreamMultiPartRequest implements MultiPartRequest { private static final Logger LOG = LoggerFactory.getLogger(SecureJakartaStreamMultiPartRequest.class); /** * Defines the internal buffer size used during streaming operations. */ private static final int BUFFER_SIZE = 10240; /** * Map between file fields and file data. */ private Map<String, List<FileInfo>> fileInfos = new HashMap<String, List<FileInfo>>(); /** * Map between non-file fields and values. */ private Map<String, List<String>> parameters = new HashMap<String, List<String>>(); /** * Internal list of raised errors to be passed to the the Struts2 framework. */ private List<String> errors = new ArrayList<String>(); /** * Internal list of non-critical messages to be passed to the Struts2 framework. */ private List<String> messages = new ArrayList<String>(); /** * Specifies the maximum size of the entire request. */ private Long maxSize; /** * Specifies the buffer size to use during streaming. */ private int bufferSize = BUFFER_SIZE; /** * Localization to be used regarding errors. */ private Locale defaultLocale = Locale.ENGLISH; public SecureJakartaStreamMultiPartRequest() { LOG.info("This is a secure implementation of the Struts Jakarta Stream Multipart parser, " + "this implementation is safe against vulnerability described in the S2-045/S2-046 Security Bulletins."); } /** * Injects the Struts multiple part maximum size. * * @param maxSize */ @Inject(StrutsConstants.STRUTS_MULTIPART_MAXSIZE) public void setMaxSize(String maxSize) { this.maxSize = Long.parseLong(maxSize); } /** * Sets the buffer size to be used. * * @param bufferSize */ @Inject(value = StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE, required = false) public void setBufferSize(String bufferSize) { this.bufferSize = Integer.parseInt(bufferSize); } /** * Injects the Struts locale provider. * * @param provider */ @Inject public void setLocaleProvider(LocaleProvider provider) { defaultLocale = provider.getLocale(); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() */ public void cleanUp() { LOG.debug("Performing File Upload temporary storage cleanup."); for (String fieldName : fileInfos.keySet()) { for (FileInfo fileInfo : fileInfos.get(fieldName)) { File file = fileInfo.getFile(); LOG.debug("Deleting file '#0'.", file.getName()); if (!file.delete()) LOG.warn("There was a problem attempting to delete file '#0'.", file.getName()); } } } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String) */ public String[] getContentType(String fieldName) { List<FileInfo> infos = fileInfos.get(fieldName); if (infos == null) return null; List<String> types = new ArrayList<String>(infos.size()); for (FileInfo fileInfo : infos) types.add(fileInfo.getContentType()); return types.toArray(new String[types.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors() */ public List<String> getErrors() { return errors; } /** * Allows interceptor to fetch non-critical messages that can be passed to the action. * * @return */ public List<String> getMesssages() { return messages; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String) */ public File[] getFile(String fieldName) { List<FileInfo> infos = fileInfos.get(fieldName); if (infos == null) return null; List<File> files = new ArrayList<File>(infos.size()); for (FileInfo fileInfo : infos) files.add(fileInfo.getFile()); return files.toArray(new File[files.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String) */ public String[] getFileNames(String fieldName) { List<FileInfo> infos = fileInfos.get(fieldName); if (infos == null) return null; List<String> names = new ArrayList<String>(infos.size()); for (FileInfo fileInfo : infos) names.add(getCanonicalName(fileInfo.getOriginalName())); return names.toArray(new String[names.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames() */ public Enumeration<String> getFileParameterNames() { return Collections.enumeration(fileInfos.keySet()); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String) */ public String[] getFilesystemName(String fieldName) { List<FileInfo> infos = fileInfos.get(fieldName); if (infos == null) return null; List<String> names = new ArrayList<String>(infos.size()); for (FileInfo fileInfo : infos) names.add(fileInfo.getFile().getName()); return names.toArray(new String[names.size()]); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String) */ public String getParameter(String name) { List<String> values = parameters.get(name); if (values != null && values.size() > 0) return values.get(0); return null; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames() */ public Enumeration<String> getParameterNames() { return Collections.enumeration(parameters.keySet()); } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String) */ public String[] getParameterValues(String name) { List<String> values = parameters.get(name); if (values != null && values.size() > 0) return values.toArray(new String[values.size()]); return null; } /* (non-Javadoc) * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(javax.servlet.http.HttpServletRequest, java.lang.String) */ public void parse(HttpServletRequest request, String saveDir) throws IOException { try { setLocale(request); processUpload(request, saveDir); } catch (Exception e) { e.printStackTrace(); String errorMessage = buildErrorMessage(e, new Object[] {}); if (!errors.contains(errorMessage)) errors.add(errorMessage); } } /** * Inspect the servlet request and set the locale if one wasn't provided by * the Struts2 framework. * * @param request */ protected void setLocale(HttpServletRequest request) { if (defaultLocale == null) defaultLocale = request.getLocale(); } /** * Processes the upload. * * @param request * @param saveDir * @throws Exception */ private void processUpload(HttpServletRequest request, String saveDir) throws Exception { // Sanity check that the request is a multi-part/form-data request. if (ServletFileUpload.isMultipartContent(request)) { // Sanity check on request size. boolean requestSizePermitted = isRequestSizePermitted(request); // Interface with Commons FileUpload API // Using the Streaming API ServletFileUpload servletFileUpload = new ServletFileUpload(); FileItemIterator i = servletFileUpload.getItemIterator(request); // Iterate the file items while (i.hasNext()) { try { FileItemStream itemStream = i.next(); // If the file item stream is a form field, delegate to the // field item stream handler if (itemStream.isFormField()) { processFileItemStreamAsFormField(itemStream); } // Delegate the file item stream for a file field to the // file item stream handler, but delegation is skipped // if the requestSizePermitted check failed based on the // complete content-size of the request. else { // prevent processing file field item if request size not allowed. // also warn user in the logs. if (!requestSizePermitted) { addFileSkippedError(itemStream.getName(), request); LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize); continue; } processFileItemStreamAsFileField(itemStream, saveDir); } } catch (IOException e) { e.printStackTrace(); } } } } /** * Defines whether the request allowed based on content length. * * @param request * @return */ private boolean isRequestSizePermitted(HttpServletRequest request) { // if maxSize is specified as -1, there is no sanity check and it's // safe to return true for any request, delegating the failure // checks later in the upload process. if (maxSize == -1 || request == null) return true; return request.getContentLength() < maxSize; } /** * Get the request content length. * * @param request * @return */ private long getRequestSize(HttpServletRequest request) { long requestSize = 0; if (request != null) requestSize = request.getContentLength(); return requestSize; } /** * Add a file skipped message notification for action messages. * * @param fileName * @param request */ private void addFileSkippedError(String fileName, HttpServletRequest request) { String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded."; FileUploadBase.FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException( exceptionMessage, getRequestSize(request), maxSize); String message = buildMessage(exception, new Object[] { fileName, getRequestSize(request), maxSize }); if (!errors.contains(message)) errors.add(message); } /** * Processes the FileItemStream as a Form Field. * * @param itemStream */ private void processFileItemStreamAsFormField(FileItemStream itemStream) { String fieldName = itemStream.getFieldName(); try { List<String> values = null; String fieldValue = Streams.asString(itemStream.openStream()); if (!parameters.containsKey(fieldName)) { values = new ArrayList<String>(); parameters.put(fieldName, values); } else { values = parameters.get(fieldName); } values.add(fieldValue); } catch (IOException e) { e.printStackTrace(); LOG.warn("Failed to handle form field '#0'.", fieldName); } } /** * Processes the FileItemStream as a file field. * * @param itemStream * @param location */ private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) { File file = null; try { // Create the temporary upload file. file = createTemporaryFile(itemStream.getName(), location); if (streamFileToDisk(itemStream, file)) createFileInfoFromItemStream(itemStream, file); } catch (IOException e) { if (file != null) { try { file.delete(); } catch (SecurityException se) { se.printStackTrace(); LOG.warn("Failed to delete '#0' due to security exception above.", file.getName()); } } } } /** * Creates a temporary file based on the given filename and location. * * @param fileName * @param location * @return * @throws IOException */ private File createTemporaryFile(String fileName, String location) throws IOException { String name = fileName.substring(fileName.lastIndexOf('/') + 1).substring(fileName.lastIndexOf('\\') + 1); String prefix = name; String suffix = ""; if (name.contains(".")) { prefix = name.substring(0, name.lastIndexOf('.')); suffix = name.substring(name.lastIndexOf('.')); } File file = File.createTempFile(prefix + "_", suffix, new File(location)); LOG.debug("Creating temporary file '#0' (originally '#1').", file.getName(), fileName); return file; } /** * Streams the file upload stream to the specified file. * * @param itemStream * @param file * @return * @throws IOException */ private boolean streamFileToDisk(FileItemStream itemStream, File file) throws IOException { boolean result = false; InputStream input = itemStream.openStream(); OutputStream output = null; try { output = new BufferedOutputStream(new FileOutputStream(file), bufferSize); byte[] buffer = new byte[bufferSize]; LOG.debug("Streaming file using buffer size #0.", bufferSize); for (int length = 0; ((length = input.read(buffer)) > 0);) output.write(buffer, 0, length); result = true; } finally { if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; } /** * Creates an internal <code>FileInfo</code> structure used to pass information * to the <code>FileUploadInterceptor</code> during the interceptor stack * invocation process. * * @param itemStream * @param file */ private void createFileInfoFromItemStream(FileItemStream itemStream, File file) { // gather attributes from file upload stream. String fileName = itemStream.getName(); String fieldName = itemStream.getFieldName(); // create internal structure FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName); // append or create new entry. if (!fileInfos.containsKey(fieldName)) { List<FileInfo> infos = new ArrayList<FileInfo>(); infos.add(fileInfo); fileInfos.put(fieldName, infos); } else { fileInfos.get(fieldName).add(fileInfo); } } /** * Get the canonical name based on the supplied filename. * * @param fileName * @return */ private String getCanonicalName(String fileName) { int forwardSlash = fileName.lastIndexOf("/"); int backwardSlash = fileName.lastIndexOf("\\"); if (forwardSlash != -1 && forwardSlash > backwardSlash) { fileName = fileName.substring(forwardSlash + 1, fileName.length()); } else { fileName = fileName.substring(backwardSlash + 1, fileName.length()); } return fileName; } /** * Build error message. * * @param e * @param args * @return */ private String buildErrorMessage(Throwable e, Object[] args) { String errorKey = "struts.message.upload.error." + e.getClass().getSimpleName(); if (LOG.isDebugEnabled()) { LOG.debug("Preparing error message for key: [#0]", errorKey); } if (LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, new Object[0]) == null) { return LocalizedTextUtil.findText(this.getClass(), "struts.messages.error.uploading", defaultLocale, null, new Object[] { e.getMessage() }); } else { return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, args); } } /** * Build action message. * * @param e * @param args * @return */ private String buildMessage(Throwable e, Object[] args) { String messageKey = "struts.message.upload.message." + e.getClass().getSimpleName(); if (LOG.isDebugEnabled()) { LOG.debug("Preparing message for key: [#0]", messageKey); } if (LocalizedTextUtil.findText(this.getClass(), messageKey, defaultLocale, null, new Object[0]) == null) { return LocalizedTextUtil.findText(this.getClass(), "struts.messages.error.uploading", defaultLocale, null, new Object[] { e.getMessage() }); } else { return LocalizedTextUtil.findText(this.getClass(), messageKey, defaultLocale, null, args); } } /** * Internal data structure used to store a reference to information needed * to later pass post processing data to the <code>FileUploadInterceptor</code>. * * @version $Revision$ * @since 7.0.0 */ private static class FileInfo implements Serializable { private File file; private String contentType; private String originalName; /** * Default constructor. * * @param file * @param contentType * @param originalName */ public FileInfo(File file, String contentType, String originalName) { this.file = file; this.contentType = contentType; this.originalName = originalName; } /** * @return */ public File getFile() { return file; } /** * @return */ public String getContentType() { return contentType; } /** * @return */ public String getOriginalName() { return originalName; } } }