package com.elibom.jogger.http.servlet.multipart; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import com.elibom.jogger.http.FileItem; /** * Provides methods to check and parse multipart requests. * * @author German Escobar */ public class Multipart { /** * HTTP content type header name. */ public static final String CONTENT_TYPE = "Content-Type"; /** * HTTP content disposition header name. */ public static final String CONTENT_DISPOSITION = "Content-Disposition"; /** * HTTP content length header name. */ public static final String CONTENT_LENGTH = "Content-Length"; /** * Content-disposition value for form data. */ public static final String FORM_DATA = "form-data"; /** * Content-disposition value for file attachment. */ public static final String ATTACHMENT = "attachment"; /** * Part of HTTP content type header. */ public static final String MULTIPART = "multipart/"; /** * HTTP content type header for multiple uploads. */ public static final String MULTIPART_MIXED = "multipart/mixed"; /** * Tells if a request is multipart or not. * * @param request the javax.servlet.http.HttpServletRequest that we are going to check. * * @return true if the request is multipart, false otherwise. */ public static boolean isMultipartContent(HttpServletRequest request) { if (!"post".equals(request.getMethod().toLowerCase())) { return false; } String contentType = request.getContentType(); if (contentType == null) { return false; } if (contentType.toLowerCase().startsWith(MULTIPART)) { return true; } return false; } /** * Parses a multipart request calling the {@link PartHandler} callbacks when a part is found. * * @param request the javax.servlet.http.HttpServletRequest that we are going to parse. * @param partHandler a callback handler that will be called when a part is found. * * @throws IOException if there is a problem parsing the request. * @throws MultipartException if the request is not multipart or if there is an error parsing the multipart * request. */ public void parse(HttpServletRequest request, PartHandler partHandler) throws IOException, MultipartException { if (!isMultipartContent(request)) { throw new MultipartException("Not a multipart content. The HTTP method should be 'POST' and the " + "Content-Type 'multipart/form-data' or 'multipart/mixed'."); } InputStream inputStream = request.getInputStream(); String contentType = request.getContentType(); String charEncoding = request.getCharacterEncoding(); byte[] boundary = getBoundary(contentType); if (boundary == null) { throw new MultipartException("the request was rejected because no multipart boundary was found"); } // create a multipart reader MultipartReader multipartReader = new MultipartReader(inputStream, boundary); multipartReader.setHeaderEncoding(charEncoding); String currentFieldName = null; boolean skipPreamble = true; for (;;) { boolean nextPart; if (skipPreamble) { nextPart = multipartReader.skipPreamble(); } else { nextPart = multipartReader.readBoundary(); } if (!nextPart) { if (currentFieldName == null) { // outer multipart terminated -> no more data return; } // inner multipart terminated -> return to parsing the outer multipartReader.setBoundary(boundary); currentFieldName = null; continue; } String headersString = multipartReader.readHeaders(); Map<String,String> headers = getHeadersMap(headersString); if (currentFieldName == null) { // we're parsing the outer multipart String fieldName = getFieldName( headers.get(CONTENT_DISPOSITION) ); if (fieldName != null) { String partContentType = headers.get(CONTENT_TYPE); if (partContentType != null && partContentType.toLowerCase().startsWith(MULTIPART_MIXED)) { // multiple files associated with this field name currentFieldName = fieldName; multipartReader.setBoundary( getBoundary(partContentType)); skipPreamble = true; continue; } String fileName = getFileName( headers.get(CONTENT_DISPOSITION) ); if (fileName == null) { // call the part handler String value = Streams.asString( multipartReader.newInputStream() ); partHandler.handleFormItem(fieldName, value); } else { // create the temp file File tempFile = createTempFile(multipartReader); // call the part handler FileItem fileItem = new FileItem(fieldName, fileName, partContentType, tempFile.length(), tempFile, headers); partHandler.handleFileItem(fieldName, fileItem); } continue; } } else { String fileName = getFileName( headers.get(CONTENT_DISPOSITION) ); String partContentType = headers.get(CONTENT_TYPE); if (fileName != null) { // create the temp file File tempFile = createTempFile(multipartReader); // call the part handler FileItem fileItem = new FileItem(currentFieldName, fileName, partContentType, tempFile.length(), tempFile, headers); partHandler.handleFileItem(currentFieldName, fileItem); continue; } } multipartReader.discardBodyData(); } } private File createTempFile(MultipartReader multipartReader) throws IOException { File tempFile = File.createTempFile("com.elibom.jogger.file_", null); FileOutputStream outputStream = null; try { outputStream = new FileOutputStream(tempFile); copy( multipartReader.newInputStream(), outputStream ); } finally { if (outputStream != null) { try { outputStream.close(); } catch (Exception e) {} } } return tempFile; } private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; private static final int EOF = -1; private long copy(InputStream input, OutputStream output) throws IOException { long count = 0; int n = 0; byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; while (EOF != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } return count; } /** * Retreives a map with the headers of a part. * * @param headerPart a String object with the contents of the part header. * * @return a Map<String,String> object with the headers of the part. */ protected Map<String,String> getHeadersMap(String headerPart) { final int len = headerPart.length(); final Map<String,String> headers = new HashMap<String,String>(); int start = 0; for (;;) { int end = parseEndOfLine(headerPart, start); if (start == end) { break; } String header = headerPart.substring(start, end); start = end + 2; while (start < len) { int nonWs = start; while (nonWs < len) { char c = headerPart.charAt(nonWs); if (c != ' ' && c != '\t') { break; } ++nonWs; } if (nonWs == start) { break; } // continuation line found end = parseEndOfLine(headerPart, nonWs); header += " " + headerPart.substring(nonWs, end); start = end + 2; } // parse header line final int colonOffset = header.indexOf(':'); if (colonOffset == -1) { // this header line is malformed, skip it. continue; } String headerName = header.substring(0, colonOffset).trim(); String headerValue = header.substring(header.indexOf(':') + 1).trim(); if (headers.containsKey(headerName)) { headers.put( headerName, headers.get(headerName) + "," + headerValue ); } else { headers.put(headerName, headerValue); } } return headers; } /** * Skips bytes until the end of the current line. * * @param headerPart the headers, which are being parsed. * @param end index of the last byte, which has yet been processed. * * @return index of the \r\n sequence, which indicates end of line. */ private int parseEndOfLine(String headerPart, int end) { int index = end; for (;;) { int offset = headerPart.indexOf('\r', index); if (offset == -1 || offset + 1 >= headerPart.length()) { throw new IllegalStateException("Expected headers to be terminated by an empty line."); } if (headerPart.charAt(offset + 1) == '\n') { return offset; } index = offset + 1; } } /** * Retrieves the name of the field from the Content-Disposition header of the part. * * @param contentDisposition the value of the Content-Disposition header. * * @return a String object that holds the name of the field to which this part is associated. */ private String getFieldName(String contentDisposition) { String fieldName = null; if (contentDisposition != null && contentDisposition.toLowerCase().startsWith(FORM_DATA)) { ParameterParser parser = new ParameterParser(); parser.setLowerCaseNames(true); // parameter parser can handle null input Map<String,String> params = parser.parse(contentDisposition, ';'); fieldName = (String) params.get("name"); if (fieldName != null) { fieldName = fieldName.trim(); } } return fieldName; } /** * Retrieves the boundary that is used to separate the request parts from the Content-Type header. * * @param contentType the value of the Content-Type header. * * @return a byte array with the boundary. */ protected byte[] getBoundary(String contentType) { ParameterParser parser = new ParameterParser(); parser.setLowerCaseNames(true); // Parameter parser can handle null input Map<String,String> params = parser.parse(contentType, new char[] {';', ','}); String boundaryStr = (String) params.get("boundary"); if (boundaryStr == null) { return null; } byte[] boundary; try { boundary = boundaryStr.getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException e) { boundary = boundaryStr.getBytes(); } return boundary; } /** * Retrieves the file name of a file from the filename attribute of the Content-Disposition header of the part. * * @param contentDisposition the value of the Content-Disposition header. * * @return a String object that holds the name of the file. */ private String getFileName(String contentDisposition) { String fileName = null; if (contentDisposition != null) { String cdl = contentDisposition.toLowerCase(); if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) { ParameterParser parser = new ParameterParser(); parser.setLowerCaseNames(true); // parameter parser can handle null input Map<String,String> params = parser.parse(contentDisposition, ';'); if (params.containsKey("filename")) { fileName = (String) params.get("filename"); if (fileName != null) { fileName = fileName.trim(); } else { // even if there is no value, the parameter is present, // so we return an empty file name rather than no file // name. fileName = ""; } } } } return fileName; } }