001: /*
002: * Copyright 2001-2007 Geert Bevin <gbevin[remove] at uwyn dot com>
003: * Inspired by code written by Jason Hunter, Jason Pell, Changshin Lee,
004: * Nic Ferrier, Michael Alyn Miller, Scott Stark, Daniel Lemire, Henri Tourigny,
005: * David Wall, Luke Blaikie
006: * Distributed under the terms of either:
007: * - the common development and distribution license (CDDL), v1.0; or
008: * - the GNU Lesser General Public License, v2.1 or later
009: * $Id: MultipartRequest.java 3634 2007-01-08 21:42:24Z gbevin $
010: */
011: package com.uwyn.rife.servlet;
012:
013: import com.uwyn.rife.engine.exceptions.*;
014: import java.io.*;
015:
016: import com.uwyn.rife.config.RifeConfig;
017: import com.uwyn.rife.engine.UploadedFile;
018: import java.util.ArrayList;
019: import java.util.HashMap;
020: import java.util.Map;
021: import javax.servlet.ServletInputStream;
022: import javax.servlet.http.HttpServletRequest;
023:
024: class MultipartRequest {
025: private static final String CONTENT_TYPE_HEADER = "Content-Type";
026: private static final String MULTIPART_CONTENT_TYPE = "multipart/form-data";
027: private static final String BOUNDARY_PREFIX = "boundary=";
028: private static final int BOUNDARY_PREFIX_LENGTH = BOUNDARY_PREFIX
029: .length();
030: private static final String CONTENT_DISPOSITION_PREFIX = "content-disposition: ";
031: private static final int CONTENT_DISPOSITION_PREFIX_LENGTH = CONTENT_DISPOSITION_PREFIX
032: .length();
033: private static final String FIELD_NAME_PREFIX = "name=\"";
034: private static final int FIELD_NAME_PREFIX_LENGTH = FIELD_NAME_PREFIX
035: .length();
036: private static final String FILENAME_PREFIX = "filename=\"";
037: private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX
038: .length();
039: private static final String QUOTE = "\"";
040: private static final String FORM_DATA_DISPOSITION = "form-data";
041: private static final String DEFAULT_ENCODING = "UTF-8";
042:
043: private File mUploadDirectory = null;
044:
045: private HttpServletRequest mRequest = null;
046: private String mBoundary = null;
047: private ServletInputStream mInput = null;
048: private byte[] mParameterBuffer = null;
049: private byte[] mFileBuffer = null;
050: private String mEncoding = DEFAULT_ENCODING;
051:
052: private HashMap<String, String[]> mParameters = null;
053: private HashMap<String, UploadedFile[]> mFiles = null;
054:
055: MultipartRequest(HttpServletRequest request)
056: throws MultipartRequestException {
057: if (null == request)
058: throw new IllegalArgumentException("request can't be null");
059:
060: mRequest = request;
061: mParameters = new HashMap<String, String[]>();
062: mFiles = new HashMap<String, UploadedFile[]>();
063: mParameterBuffer = new byte[8 * 1024];
064: mFileBuffer = new byte[100 * 1024];
065:
066: checkUploadDirectory();
067: initialize();
068: checkInputStart();
069: readParts();
070: }
071:
072: static boolean isValidContentType(String type) {
073: if (null == type
074: || !type.toLowerCase().startsWith(
075: MULTIPART_CONTENT_TYPE)) {
076: return false;
077: }
078:
079: return true;
080: }
081:
082: Map<String, String[]> getParameterMap() {
083: return mParameters;
084: }
085:
086: Map<String, UploadedFile[]> getFileMap() {
087: return mFiles;
088: }
089:
090: void setEncoding(String encoding) {
091: assert encoding != null;
092:
093: mEncoding = encoding;
094: }
095:
096: private void checkUploadDirectory()
097: throws MultipartRequestException {
098: mUploadDirectory = new File(RifeConfig.Engine
099: .getFileUploadPath());
100: mUploadDirectory.mkdirs();
101:
102: if (!mUploadDirectory.exists()
103: || !mUploadDirectory.isDirectory()
104: || !mUploadDirectory.canWrite()) {
105: throw new MultipartInvalidUploadDirectoryException(
106: mUploadDirectory);
107: }
108: }
109:
110: private void initialize() throws MultipartRequestException {
111: // Check the content type to is correct to support a multipart request
112: // Access header two ways to work around WebSphere oddities
113: String type = null;
114: String type_header = mRequest.getHeader(CONTENT_TYPE_HEADER);
115: String type_method = mRequest.getContentType();
116:
117: // If one value is null, choose the other value
118: if (type_header == null && type_method != null) {
119: type = type_method;
120: } else if (type_method == null && type_header != null) {
121: type = type_header;
122: }
123: // If neither value is null, choose the longer value
124: else if (type_header != null && type_method != null) {
125: type = (type_header.length() > type_method.length() ? type_header
126: : type_method);
127: }
128:
129: // ensure that the content-type is correct
130: if (!isValidContentType(type)) {
131: throw new MultipartInvalidContentTypeException(type);
132: }
133:
134: // extract the boundary seperator that is used by this request
135: mBoundary = extractBoundary(type);
136: if (null == mBoundary) {
137: throw new MultipartMissingBoundaryException();
138: }
139:
140: // obtain the input stream
141: try {
142: mInput = mRequest.getInputStream();
143: } catch (IOException e) {
144: throw new MultipartInputErrorException(e);
145: }
146: }
147:
148: private void checkInputStart() throws MultipartRequestException {
149: // Read the first line, should be the first boundary
150: String line = readLine();
151: if (null == line) {
152: throw new MultipartUnexpectedEndingException();
153: }
154:
155: // Verify that the line is the boundary
156: if (!line.startsWith(mBoundary)) {
157: throw new MultipartInvalidBoundaryException(mBoundary, line);
158: }
159: }
160:
161: private void readParts() throws MultipartRequestException {
162: boolean more_parts = true;
163:
164: while (more_parts) {
165: more_parts = readNextPart();
166: }
167: }
168:
169: private String extractBoundary(String line) {
170: // Use lastIndexOf() because IE 4.01 on Win98 has been known to send the
171: // "boundary=" string multiple times.
172: int index = line.lastIndexOf(BOUNDARY_PREFIX);
173:
174: if (-1 == index) {
175: return null;
176: }
177:
178: // start from after the boundary prefix
179: String boundary = line
180: .substring(index + BOUNDARY_PREFIX_LENGTH);
181: if ('"' == boundary.charAt(0)) {
182: // The boundary is enclosed in quotes, strip them
183: index = boundary.lastIndexOf('"');
184: boundary = boundary.substring(1, index);
185: }
186:
187: // The real boundary is always preceeded by an extra "--"
188: boundary = "--" + boundary;
189:
190: return boundary;
191: }
192:
193: private String readLine() throws MultipartRequestException {
194: StringBuilder line_buffer = new StringBuilder();
195:
196: int result = 0;
197: do {
198: try {
199: result = mInput.readLine(mParameterBuffer, 0,
200: mParameterBuffer.length);
201: } catch (IOException e) {
202: throw new MultipartInputErrorException(e);
203: }
204:
205: if (result != -1) {
206: try {
207: line_buffer.append(new String(mParameterBuffer, 0,
208: result, mEncoding));
209: } catch (UnsupportedEncodingException e) {
210: throw new MultipartInputErrorException(e);
211: }
212: }
213: }
214: // if the buffer wasn't completely filled, the end of the input has been reached
215: while (result == mParameterBuffer.length);
216:
217: // if nothing was read, the end of the stream must have been reached
218: if (line_buffer.length() == 0) {
219: return null;
220: }
221:
222: // Cut off the trailing \n or \r\n
223: // It should always be \r\n but IE5 sometimes does just \n
224: int line_length = line_buffer.length();
225: if (line_length >= 2
226: && '\r' == line_buffer.charAt(line_length - 2)) {
227: // remove the trailing \r\n
228: line_buffer.setLength(line_length - 2);
229: } else if (line_length >= 1
230: && '\n' == line_buffer.charAt(line_length - 1)) {
231: // remove the trailing \n
232: line_buffer.setLength(line_length - 1);
233: }
234:
235: return line_buffer.toString();
236: }
237:
238: private boolean readNextPart() throws MultipartRequestException {
239: // Read the headers; they look like this (not all may be present):
240: // Content-Disposition: form-data; name="field1"; filename="file1.txt"
241: // Content-Type: type/subtype
242: // Content-Transfer-Encoding: binary
243: ArrayList<String> headers = new ArrayList<String>();
244:
245: String line = readLine();
246: // When no next line could be read, the end was reached.
247: // IE4 on Mac sends an empty line at the end; treat that as the ending too.
248: if (null == line || 0 == line.length()) {
249: // No parts left, we're done
250: return false;
251: }
252:
253: // Read the following header lines we hit an empty line
254: // A line starting with whitespace is considered a continuation;
255: // that requires a little special logic.
256: while (null != line && line.length() > 0) {
257: String next_line = null;
258: boolean obtain_next_line = true;
259: while (obtain_next_line) {
260: next_line = readLine();
261:
262: if (next_line != null
263: && (next_line.startsWith(" ") || next_line
264: .startsWith("\t"))) {
265: line = line + next_line;
266: } else {
267: obtain_next_line = false;
268: }
269: }
270: // Add the line to the header list
271: headers.add(line);
272: line = next_line;
273: }
274:
275: // If we got a null above, it's the end
276: if (line == null) {
277: return false;
278: }
279:
280: String fieldname = null;
281: String filename = null;
282: String content_type = "text/plain"; // rfc1867 says this is the default
283:
284: String[] disposition_info = null;
285:
286: for (String headerline : headers) {
287: if (headerline.toLowerCase().startsWith(
288: CONTENT_DISPOSITION_PREFIX)) {
289: // Parse the content-disposition line
290: disposition_info = extractDispositionInfo(headerline);
291:
292: fieldname = disposition_info[0];
293: filename = disposition_info[1];
294: } else if (headerline.toLowerCase().startsWith(
295: CONTENT_TYPE_HEADER)) {
296: // Get the content type, or null if none specified
297: String type = extractContentType(headerline);
298: if (type != null) {
299: content_type = type;
300: }
301: }
302: }
303:
304: if (null == filename) {
305: // This is a parameter
306: String new_value = readParameter();
307: String[] values = mParameters.get(fieldname);
308: String[] new_values = null;
309: if (null == values) {
310: new_values = new String[1];
311: } else {
312: new_values = new String[values.length + 1];
313: System.arraycopy(values, 0, new_values, 0,
314: values.length);
315: }
316: new_values[new_values.length - 1] = new_value;
317: mParameters.put(fieldname, new_values);
318: } else {
319: // This is a file
320: if (filename.equals("")) {
321: // empty filename, probably an "empty" file param
322: filename = null;
323: }
324:
325: UploadedFile new_file = new UploadedFile(filename,
326: content_type);
327: readAndSaveFile(new_file, fieldname);
328: UploadedFile[] files = mFiles.get(fieldname);
329: UploadedFile[] new_files = null;
330: if (null == files) {
331: new_files = new UploadedFile[1];
332: } else {
333: new_files = new UploadedFile[files.length + 1];
334: System.arraycopy(files, 0, new_files, 0, files.length);
335: }
336: new_files[new_files.length - 1] = new_file;
337: mFiles.put(fieldname, new_files);
338: }
339:
340: return true;
341: }
342:
343: private String[] extractDispositionInfo(String dispositionLine)
344: throws MultipartRequestException {
345: // Return the line's data as an array: disposition, name, filename, full filename
346: String[] result = new String[3];
347: String lowcase_line = dispositionLine.toLowerCase();
348: String fieldname = null;
349: String filename = null;
350: String filename_full = null;
351:
352: // Get the content disposition, should be "form-data"
353: int start = lowcase_line.indexOf(CONTENT_DISPOSITION_PREFIX);
354: int end = lowcase_line.indexOf(";");
355: if (-1 == start || -1 == end) {
356: throw new MultipartCorruptContentDispositionException(
357: dispositionLine);
358: }
359: String disposition = lowcase_line.substring(start
360: + CONTENT_DISPOSITION_PREFIX_LENGTH, end);
361: if (!disposition.equals(FORM_DATA_DISPOSITION)) {
362: throw new MultipartInvalidContentDispositionException(
363: dispositionLine);
364: }
365:
366: // Get the field name, start at last semicolon
367: start = lowcase_line.indexOf(FIELD_NAME_PREFIX, end);
368: end = lowcase_line.indexOf(QUOTE, start
369: + FIELD_NAME_PREFIX_LENGTH);
370: if (-1 == start || -1 == end) {
371: throw new MultipartCorruptContentDispositionException(
372: dispositionLine);
373: }
374: fieldname = dispositionLine.substring(start
375: + FIELD_NAME_PREFIX_LENGTH, end);
376:
377: // Get the filename, if given
378: start = lowcase_line.indexOf(FILENAME_PREFIX, end + 2); // after quote and space)
379: end = lowcase_line.indexOf(QUOTE, start
380: + FILENAME_PREFIX_LENGTH);
381: if (start != -1 && end != -1) {
382: filename_full = dispositionLine.substring(start
383: + FILENAME_PREFIX_LENGTH, end);
384: filename = filename_full;
385:
386: // The filename may contain a full path. Cut to just the filename.
387: int last_slash = Math.max(filename.lastIndexOf('/'),
388: filename.lastIndexOf('\\'));
389: if (last_slash > -1) {
390: // only take the filename (after the last slash)
391: filename = filename.substring(last_slash + 1);
392: }
393: }
394:
395: // Return a String array: name, filename, full filename
396: // empty filename denotes no file posted!
397: result[0] = fieldname;
398: result[1] = filename;
399: result[2] = filename_full;
400:
401: return result;
402: }
403:
404: private String extractContentType(String contentTypeLine)
405: throws MultipartRequestException {
406: String result = null;
407: String lowcase_line = contentTypeLine.toLowerCase();
408:
409: // Get the content type, if any
410: if (lowcase_line.startsWith(CONTENT_TYPE_HEADER)) {
411: int seperator_location = lowcase_line.indexOf(" ");
412: if (-1 == seperator_location) {
413: throw new MultipartCorruptContentTypeException(
414: contentTypeLine);
415: }
416: result = lowcase_line.substring(seperator_location + 1);
417: } else if (lowcase_line.length() != 0) {
418: // no content type, so should be empty
419: throw new MultipartCorruptContentTypeException(
420: contentTypeLine);
421: }
422:
423: return result;
424: }
425:
426: private String readParameter() throws MultipartRequestException {
427: StringBuilder result = new StringBuilder();
428: String line = null;
429: while ((line = readLine()) != null) {
430: if (line.startsWith(mBoundary)) {
431: break;
432: }
433: // add the \r\n in case there are many lines
434: result.append(line).append("\r\n");
435: }
436:
437: // nothing read
438: if (0 == result.length()) {
439: return null;
440: }
441:
442: // cut off the last line's \r\n
443: result.setLength(result.length() - 2);
444:
445: return result.toString();
446: }
447:
448: private void readAndSaveFile(UploadedFile file, String name)
449: throws MultipartRequestException {
450: assert file != null;
451:
452: File tmp_file = null;
453: FileOutputStream output_stream = null;
454: BufferedOutputStream output = null;
455:
456: try {
457: tmp_file = File.createTempFile("upl", ".tmp",
458: mUploadDirectory);
459: } catch (IOException e) {
460: throw new MultipartFileErrorException(name, e);
461: }
462: try {
463: output_stream = new FileOutputStream(tmp_file);
464: } catch (FileNotFoundException e) {
465: throw new MultipartFileErrorException(name, e);
466: }
467: output = new BufferedOutputStream(output_stream, 8 * 1024); // 8K
468:
469: long downloaded_size = 0;
470: int result = -1;
471: String line = null;
472: int line_length = 0;
473:
474: // ServletInputStream.readLine() has the annoying habit of
475: // adding a \r\n to the end of the last line.
476: // Since we want a byte-for-byte transfer, we have to cut those chars.
477: boolean rnflag = false;
478: try {
479: while ((result = mInput.readLine(mFileBuffer, 0,
480: mFileBuffer.length)) != -1) {
481: // Check for boundary
482: if (result > 2 && '-' == mFileBuffer[0]
483: && '-' == mFileBuffer[1]) {
484: // quick pre-check
485: try {
486: line = new String(mFileBuffer, 0, result,
487: mEncoding);
488: } catch (UnsupportedEncodingException e) {
489: throw new MultipartFileErrorException(name, e);
490: }
491:
492: if (line.startsWith(mBoundary)) {
493: break;
494: }
495: }
496:
497: // Are we supposed to write \r\n for the last iteration?
498: if (rnflag && output != null) {
499: output.write('\r');
500: output.write('\n');
501: rnflag = false;
502: }
503:
504: // postpone any ending \r\n
505: if (result >= 2 && '\r' == mFileBuffer[result - 2]
506: && '\n' == mFileBuffer[result - 1]) {
507: line_length = result - 2; // skip the last 2 chars
508: rnflag = true; // make a note to write them on the next iteration
509: } else {
510: line_length = result;
511: }
512:
513: // increase size count
514: if (output != null
515: && RifeConfig.Engine.getFileUploadSizeCheck()) {
516: downloaded_size += line_length;
517:
518: if (downloaded_size > RifeConfig.Engine
519: .getFileuploadSizeLimit()) {
520: file.setSizeExceeded(true);
521: output.close();
522: output = null;
523: tmp_file.delete();
524: tmp_file = null;
525: if (RifeConfig.Engine
526: .getFileUploadSizeException()) {
527: throw new MultipartFileTooBigException(
528: name, RifeConfig.Engine
529: .getFileuploadSizeLimit());
530: }
531: }
532: }
533:
534: // write the content
535: if (output != null) {
536: output.write(mFileBuffer, 0, line_length);
537: }
538: }
539: } catch (IOException e) {
540: throw new MultipartFileErrorException(name, e);
541: } finally {
542: try {
543: if (output != null) {
544: output.flush();
545: output.close();
546: output_stream.close();
547: }
548: } catch (IOException e) {
549: throw new MultipartFileErrorException(name, e);
550: }
551: }
552:
553: if (tmp_file != null) {
554: file.setTempFile(tmp_file);
555: }
556: }
557: }
|