/* * Copyright 2001-2008 Geert Bevin <gbevin[remove] at uwyn dot com> * Inspired by code written by Jason Hunter, Jason Pell, Changshin Lee, * Nic Ferrier, Michael Alyn Miller, Scott Stark, Daniel Lemire, Henri Tourigny, * David Wall, Luke Blaikie * Licensed under the Apache License, Version 2.0 (the "License") * $Id: MultipartRequest.java 3918 2008-04-14 17:35:35Z gbevin $ */ package com.uwyn.rife.servlet; import com.uwyn.rife.engine.exceptions.*; import java.io.*; import com.uwyn.rife.config.RifeConfig; import com.uwyn.rife.engine.UploadedFile; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; class MultipartRequest { private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String MULTIPART_CONTENT_TYPE = "multipart/form-data"; private static final String BOUNDARY_PREFIX = "boundary="; private static final int BOUNDARY_PREFIX_LENGTH = BOUNDARY_PREFIX.length(); private static final String CONTENT_DISPOSITION_PREFIX = "content-disposition: "; private static final int CONTENT_DISPOSITION_PREFIX_LENGTH = CONTENT_DISPOSITION_PREFIX.length(); private static final String FIELD_NAME_PREFIX = "name=\""; private static final int FIELD_NAME_PREFIX_LENGTH = FIELD_NAME_PREFIX.length(); private static final String FILENAME_PREFIX = "filename=\""; private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); private static final String QUOTE = "\""; private static final String FORM_DATA_DISPOSITION = "form-data"; private static final String DEFAULT_ENCODING = "UTF-8"; private File mUploadDirectory = null; private HttpServletRequest mRequest = null; private String mBoundary = null; private ServletInputStream mInput = null; private byte[] mParameterBuffer = null; private byte[] mFileBuffer = null; private String mEncoding = DEFAULT_ENCODING; private HashMap<String, String[]> mParameters = null; private HashMap<String, UploadedFile[]> mFiles = null; MultipartRequest(HttpServletRequest request) throws MultipartRequestException { if (null == request) throw new IllegalArgumentException("request can't be null"); mRequest = request; mParameters = new HashMap<String, String[]>(); mFiles = new HashMap<String, UploadedFile[]>(); mParameterBuffer = new byte[8 * 1024]; mFileBuffer = new byte[100 * 1024]; checkUploadDirectory(); initialize(); checkInputStart(); readParts(); } static boolean isValidContentType(String type) { if (null == type || !type.toLowerCase().startsWith(MULTIPART_CONTENT_TYPE)) { return false; } return true; } Map<String, String[]> getParameterMap() { return mParameters; } Map<String, UploadedFile[]> getFileMap() { return mFiles; } void setEncoding(String encoding) { assert encoding != null; mEncoding = encoding; } private void checkUploadDirectory() throws MultipartRequestException { mUploadDirectory = new File(RifeConfig.Engine.getFileUploadPath()); mUploadDirectory.mkdirs(); if (!mUploadDirectory.exists() || !mUploadDirectory.isDirectory() || !mUploadDirectory.canWrite()) { throw new MultipartInvalidUploadDirectoryException(mUploadDirectory); } } private void initialize() throws MultipartRequestException { // Check the content type to is correct to support a multipart request // Access header two ways to work around WebSphere oddities String type = null; String type_header = mRequest.getHeader(CONTENT_TYPE_HEADER); String type_method = mRequest.getContentType(); // If one value is null, choose the other value if (type_header == null && type_method != null) { type = type_method; } else if (type_method == null && type_header != null) { type = type_header; } // If neither value is null, choose the longer value else if (type_header != null && type_method != null) { type = (type_header.length() > type_method.length() ? type_header : type_method); } // ensure that the content-type is correct if (!isValidContentType(type)) { throw new MultipartInvalidContentTypeException(type); } // extract the boundary seperator that is used by this request mBoundary = extractBoundary(type); if (null == mBoundary) { throw new MultipartMissingBoundaryException(); } // obtain the input stream try { mInput = mRequest.getInputStream(); } catch (IOException e) { throw new MultipartInputErrorException(e); } } private void checkInputStart() throws MultipartRequestException { // Read the first line, should be the first boundary String line = readLine(); if (null == line) { throw new MultipartUnexpectedEndingException(); } // Verify that the line is the boundary if (!line.startsWith(mBoundary)) { throw new MultipartInvalidBoundaryException(mBoundary, line); } } private void readParts() throws MultipartRequestException { boolean more_parts = true; while (more_parts) { more_parts = readNextPart(); } } private String extractBoundary(String line) { // Use lastIndexOf() because IE 4.01 on Win98 has been known to send the // "boundary=" string multiple times. int index = line.lastIndexOf(BOUNDARY_PREFIX); if (-1 == index) { return null; } // start from after the boundary prefix String boundary = line.substring(index + BOUNDARY_PREFIX_LENGTH); if ('"' == boundary.charAt(0)) { // The boundary is enclosed in quotes, strip them index = boundary.lastIndexOf('"'); boundary = boundary.substring(1, index); } // The real boundary is always preceeded by an extra "--" boundary = "--" + boundary; return boundary; } private String readLine() throws MultipartRequestException { StringBuilder line_buffer = new StringBuilder(); int result = 0; do { try { result = mInput.readLine(mParameterBuffer, 0, mParameterBuffer.length); } catch (IOException e) { throw new MultipartInputErrorException(e); } if (result != -1) { try { line_buffer.append(new String(mParameterBuffer, 0, result, mEncoding)); } catch (UnsupportedEncodingException e) { throw new MultipartInputErrorException(e); } } } // if the buffer wasn't completely filled, the end of the input has been reached while (result == mParameterBuffer.length); // if nothing was read, the end of the stream must have been reached if (line_buffer.length() == 0) { return null; } // Cut off the trailing \n or \r\n // It should always be \r\n but IE5 sometimes does just \n int line_length = line_buffer.length(); if (line_length >= 2 && '\r' == line_buffer.charAt(line_length - 2)) { // remove the trailing \r\n line_buffer.setLength(line_length - 2); } else if (line_length >= 1 && '\n' == line_buffer.charAt(line_length - 1)) { // remove the trailing \n line_buffer.setLength(line_length - 1); } return line_buffer.toString(); } private boolean readNextPart() throws MultipartRequestException { // Read the headers; they look like this (not all may be present): // Content-Disposition: form-data; name="field1"; filename="file1.txt" // Content-Type: type/subtype // Content-Transfer-Encoding: binary ArrayList<String> headers = new ArrayList<String>(); String line = readLine(); // When no next line could be read, the end was reached. // IE4 on Mac sends an empty line at the end; treat that as the ending too. if (null == line || 0 == line.length()) { // No parts left, we're done return false; } // Read the following header lines we hit an empty line // A line starting with whitespace is considered a continuation; // that requires a little special logic. while (null != line && line.length() > 0) { String next_line = null; boolean obtain_next_line = true; while (obtain_next_line) { next_line = readLine(); if (next_line != null && (next_line.startsWith(" ") || next_line.startsWith("\t"))) { line = line + next_line; } else { obtain_next_line = false; } } // Add the line to the header list headers.add(line); line = next_line; } // If we got a null above, it's the end if (line == null) { return false; } String fieldname = null; String filename = null; String content_type = "text/plain"; // rfc1867 says this is the default String[] disposition_info = null; for (String headerline : headers) { if (headerline.toLowerCase().startsWith(CONTENT_DISPOSITION_PREFIX)) { // Parse the content-disposition line disposition_info = extractDispositionInfo(headerline); fieldname = disposition_info[0]; filename = disposition_info[1]; } else if (headerline.toLowerCase().startsWith(CONTENT_TYPE_HEADER)) { // Get the content type, or null if none specified String type = extractContentType(headerline); if (type != null) { content_type = type; } } } if (null == filename) { // This is a parameter String new_value = readParameter(); String[] values = mParameters.get(fieldname); String[] new_values = null; if (null == values) { new_values = new String[1]; } else { new_values = new String[values.length + 1]; System.arraycopy(values, 0, new_values, 0, values.length); } new_values[new_values.length - 1] = new_value; mParameters.put(fieldname, new_values); } else { // This is a file if (filename.equals("")) { // empty filename, probably an "empty" file param filename = null; } UploadedFile new_file = new UploadedFile(filename, content_type); readAndSaveFile(new_file, fieldname); UploadedFile[] files = mFiles.get(fieldname); UploadedFile[] new_files = null; if (null == files) { new_files = new UploadedFile[1]; } else { new_files = new UploadedFile[files.length + 1]; System.arraycopy(files, 0, new_files, 0, files.length); } new_files[new_files.length - 1] = new_file; mFiles.put(fieldname, new_files); } return true; } private String[] extractDispositionInfo(String dispositionLine) throws MultipartRequestException { // Return the line's data as an array: disposition, name, filename, full filename String[] result = new String[3]; String lowcase_line = dispositionLine.toLowerCase(); String fieldname = null; String filename = null; String filename_full = null; // Get the content disposition, should be "form-data" int start = lowcase_line.indexOf(CONTENT_DISPOSITION_PREFIX); int end = lowcase_line.indexOf(";"); if (-1 == start || -1 == end) { throw new MultipartCorruptContentDispositionException(dispositionLine); } String disposition = lowcase_line.substring(start + CONTENT_DISPOSITION_PREFIX_LENGTH, end); if (!disposition.equals(FORM_DATA_DISPOSITION)) { throw new MultipartInvalidContentDispositionException(dispositionLine); } // Get the field name, start at last semicolon start = lowcase_line.indexOf(FIELD_NAME_PREFIX, end); end = lowcase_line.indexOf(QUOTE, start + FIELD_NAME_PREFIX_LENGTH); if (-1 == start || -1 == end) { throw new MultipartCorruptContentDispositionException(dispositionLine); } fieldname = dispositionLine.substring(start + FIELD_NAME_PREFIX_LENGTH, end); // Get the filename, if given start = lowcase_line.indexOf(FILENAME_PREFIX, end + 2); // after quote and space) end = lowcase_line.indexOf(QUOTE, start + FILENAME_PREFIX_LENGTH); if (start != -1 && end != -1) { filename_full = dispositionLine.substring(start + FILENAME_PREFIX_LENGTH, end); filename = filename_full; // The filename may contain a full path. Cut to just the filename. int last_slash = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); if (last_slash > -1) { // only take the filename (after the last slash) filename = filename.substring(last_slash + 1); } } // Return a String array: name, filename, full filename // empty filename denotes no file posted! result[0] = fieldname; result[1] = filename; result[2] = filename_full; return result; } private String extractContentType(String contentTypeLine) throws MultipartRequestException { String result = null; String lowcase_line = contentTypeLine.toLowerCase(); // Get the content type, if any if (lowcase_line.startsWith(CONTENT_TYPE_HEADER)) { int seperator_location = lowcase_line.indexOf(" "); if (-1 == seperator_location) { throw new MultipartCorruptContentTypeException(contentTypeLine); } result = lowcase_line.substring(seperator_location + 1); } else if (lowcase_line.length() != 0) { // no content type, so should be empty throw new MultipartCorruptContentTypeException(contentTypeLine); } return result; } private String readParameter() throws MultipartRequestException { StringBuilder result = new StringBuilder(); String line = null; while ((line = readLine()) != null) { if (line.startsWith(mBoundary)) { break; } // add the \r\n in case there are many lines result.append(line).append("\r\n"); } // nothing read if (0 == result.length()) { return null; } // cut off the last line's \r\n result.setLength(result.length() - 2); return result.toString(); } private void readAndSaveFile(UploadedFile file, String name) throws MultipartRequestException { assert file != null; File tmp_file = null; FileOutputStream output_stream = null; BufferedOutputStream output = null; try { tmp_file = File.createTempFile("upl", ".tmp", mUploadDirectory); } catch (IOException e) { throw new MultipartFileErrorException(name, e); } try { output_stream = new FileOutputStream(tmp_file); } catch (FileNotFoundException e) { throw new MultipartFileErrorException(name, e); } output = new BufferedOutputStream(output_stream, 8 * 1024); // 8K long downloaded_size = 0; int result = -1; String line = null; int line_length = 0; // ServletInputStream.readLine() has the annoying habit of // adding a \r\n to the end of the last line. // Since we want a byte-for-byte transfer, we have to cut those chars. boolean rnflag = false; try { while ((result = mInput.readLine(mFileBuffer, 0, mFileBuffer.length)) != -1) { // Check for boundary if (result > 2 && '-' == mFileBuffer[0] && '-' == mFileBuffer[1]) { // quick pre-check try { line = new String(mFileBuffer, 0, result, mEncoding); } catch (UnsupportedEncodingException e) { throw new MultipartFileErrorException(name, e); } if (line.startsWith(mBoundary)) { break; } } // Are we supposed to write \r\n for the last iteration? if (rnflag && output != null) { output.write('\r'); output.write('\n'); rnflag = false; } // postpone any ending \r\n if (result >= 2 && '\r' == mFileBuffer[result - 2] && '\n' == mFileBuffer[result - 1]) { line_length = result - 2; // skip the last 2 chars rnflag = true; // make a note to write them on the next iteration } else { line_length = result; } // increase size count if (output != null && RifeConfig.Engine.getFileUploadSizeCheck()) { downloaded_size += line_length; if (downloaded_size > RifeConfig.Engine.getFileuploadSizeLimit()) { file.setSizeExceeded(true); output.close(); output = null; tmp_file.delete(); tmp_file = null; if (RifeConfig.Engine.getFileUploadSizeException()) { throw new MultipartFileTooBigException(name, RifeConfig.Engine.getFileuploadSizeLimit()); } } } // write the content if (output != null) { output.write(mFileBuffer, 0, line_length); } } } catch (IOException e) { throw new MultipartFileErrorException(name, e); } finally { try { if (output != null) { output.flush(); output.close(); output_stream.close(); } } catch (IOException e) { throw new MultipartFileErrorException(name, e); } } if (tmp_file != null) { file.setTempFile(tmp_file); } } }