blob: 6aae849b41e6f0fc838d9fb0859401ffc990704e [file] [log] [blame]
Jake Slack03928ae2014-05-13 18:41:56 -07001//
2// ========================================================================
3// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4// ------------------------------------------------------------------------
5// All rights reserved. This program and the accompanying materials
6// are made available under the terms of the Eclipse Public License v1.0
7// and Apache License v2.0 which accompanies this distribution.
8//
9// The Eclipse Public License is available at
10// http://www.eclipse.org/legal/epl-v10.html
11//
12// The Apache License v2.0 is available at
13// http://www.opensource.org/licenses/apache2.0.php
14//
15// You may elect to redistribute this code under either of these licenses.
16// ========================================================================
17//
18
19package org.eclipse.jetty.util;
20
21import java.io.BufferedInputStream;
22import java.io.BufferedOutputStream;
23import java.io.BufferedReader;
24import java.io.ByteArrayInputStream;
25import java.io.ByteArrayOutputStream;
26import java.io.File;
27import java.io.FileInputStream;
28import java.io.FileNotFoundException;
29import java.io.FileOutputStream;
30import java.io.FilterInputStream;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.InputStreamReader;
34import java.io.OutputStream;
35import java.lang.reflect.Array;
36import java.util.ArrayList;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.HashMap;
40import java.util.List;
41import java.util.Locale;
42import java.util.Map;
43import java.util.StringTokenizer;
44
45import javax.servlet.MultipartConfigElement;
46import javax.servlet.ServletException;
47import javax.servlet.http.Part;
48
49import org.eclipse.jetty.util.log.Log;
50import org.eclipse.jetty.util.log.Logger;
51
52
53
54/**
55 * MultiPartInputStream
56 *
57 * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
58 */
59public class MultiPartInputStream
60{
61 private static final Logger LOG = Log.getLogger(MultiPartInputStream.class);
62
63 public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
64 protected InputStream _in;
65 protected MultipartConfigElement _config;
66 protected String _contentType;
67 protected MultiMap<String> _parts;
68 protected File _tmpDir;
69 protected File _contextTmpDir;
70 protected boolean _deleteOnExit;
71
72
73
74 public class MultiPart implements Part
75 {
76 protected String _name;
77 protected String _filename;
78 protected File _file;
79 protected OutputStream _out;
80 protected ByteArrayOutputStream2 _bout;
81 protected String _contentType;
82 protected MultiMap<String> _headers;
83 protected long _size = 0;
84 protected boolean _temporary = true;
85
86 public MultiPart (String name, String filename)
87 throws IOException
88 {
89 _name = name;
90 _filename = filename;
91 }
92
93 protected void setContentType (String contentType)
94 {
95 _contentType = contentType;
96 }
97
98
99 protected void open()
100 throws IOException
101 {
102 //We will either be writing to a file, if it has a filename on the content-disposition
103 //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
104 //will need to change to write to a file.
105 if (_filename != null && _filename.trim().length() > 0)
106 {
107 createFile();
108 }
109 else
110 {
111 //Write to a buffer in memory until we discover we've exceed the
112 //MultipartConfig fileSizeThreshold
113 _out = _bout= new ByteArrayOutputStream2();
114 }
115 }
116
117 protected void close()
118 throws IOException
119 {
120 _out.close();
121 }
122
123
124 protected void write (int b)
125 throws IOException
126 {
127 if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStream.this._config.getMaxFileSize())
128 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
129
130 if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
131 createFile();
132 _out.write(b);
133 _size ++;
134 }
135
136 protected void write (byte[] bytes, int offset, int length)
137 throws IOException
138 {
139 if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStream.this._config.getMaxFileSize())
140 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
141
142 if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
143 createFile();
144
145 _out.write(bytes, offset, length);
146 _size += length;
147 }
148
149 protected void createFile ()
150 throws IOException
151 {
152 _file = File.createTempFile("MultiPart", "", MultiPartInputStream.this._tmpDir);
153 if (_deleteOnExit)
154 _file.deleteOnExit();
155 FileOutputStream fos = new FileOutputStream(_file);
156 BufferedOutputStream bos = new BufferedOutputStream(fos);
157
158 if (_size > 0 && _out != null)
159 {
160 //already written some bytes, so need to copy them into the file
161 _out.flush();
162 _bout.writeTo(bos);
163 _out.close();
164 _bout = null;
165 }
166 _out = bos;
167 }
168
169
170
171 protected void setHeaders(MultiMap<String> headers)
172 {
173 _headers = headers;
174 }
175
176 /**
177 * @see javax.servlet.http.Part#getContentType()
178 */
179 public String getContentType()
180 {
181 return _contentType;
182 }
183
184 /**
185 * @see javax.servlet.http.Part#getHeader(java.lang.String)
186 */
187 public String getHeader(String name)
188 {
189 if (name == null)
190 return null;
191 return (String)_headers.getValue(name.toLowerCase(Locale.ENGLISH), 0);
192 }
193
194 /**
195 * @see javax.servlet.http.Part#getHeaderNames()
196 */
197 public Collection<String> getHeaderNames()
198 {
199 return _headers.keySet();
200 }
201
202 /**
203 * @see javax.servlet.http.Part#getHeaders(java.lang.String)
204 */
205 public Collection<String> getHeaders(String name)
206 {
207 return _headers.getValues(name);
208 }
209
210 /**
211 * @see javax.servlet.http.Part#getInputStream()
212 */
213 public InputStream getInputStream() throws IOException
214 {
215 if (_file != null)
216 {
217 //written to a file, whether temporary or not
218 return new BufferedInputStream (new FileInputStream(_file));
219 }
220 else
221 {
222 //part content is in memory
223 return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
224 }
225 }
226
227 public byte[] getBytes()
228 {
229 if (_bout!=null)
230 return _bout.toByteArray();
231 return null;
232 }
233
234 /**
235 * @see javax.servlet.http.Part#getName()
236 */
237 public String getName()
238 {
239 return _name;
240 }
241
242 /**
243 * @see javax.servlet.http.Part#getSize()
244 */
245 public long getSize()
246 {
247 return _size;
248 }
249
250 /**
251 * @see javax.servlet.http.Part#write(java.lang.String)
252 */
253 public void write(String fileName) throws IOException
254 {
255 if (_file == null)
256 {
257 _temporary = false;
258
259 //part data is only in the ByteArrayOutputStream and never been written to disk
260 _file = new File (_tmpDir, fileName);
261
262 BufferedOutputStream bos = null;
263 try
264 {
265 bos = new BufferedOutputStream(new FileOutputStream(_file));
266 _bout.writeTo(bos);
267 bos.flush();
268 }
269 finally
270 {
271 if (bos != null)
272 bos.close();
273 _bout = null;
274 }
275 }
276 else
277 {
278 //the part data is already written to a temporary file, just rename it
279 _temporary = false;
280
281 File f = new File(_tmpDir, fileName);
282 if (_file.renameTo(f))
283 _file = f;
284 }
285 }
286
287 /**
288 * Remove the file, whether or not Part.write() was called on it
289 * (ie no longer temporary)
290 * @see javax.servlet.http.Part#delete()
291 */
292 public void delete() throws IOException
293 {
294 if (_file != null && _file.exists())
295 _file.delete();
296 }
297
298 /**
299 * Only remove tmp files.
300 *
301 * @throws IOException
302 */
303 public void cleanUp() throws IOException
304 {
305 if (_temporary && _file != null && _file.exists())
306 _file.delete();
307 }
308
309
310 /**
311 * Get the file, if any, the data has been written to.
312 * @return
313 */
314 public File getFile ()
315 {
316 return _file;
317 }
318
319
320 /**
321 * Get the filename from the content-disposition.
322 * @return null or the filename
323 */
324 public String getContentDispositionFilename ()
325 {
326 return _filename;
327 }
328 }
329
330
331
332
333 /**
334 * @param in Request input stream
335 * @param contentType Content-Type header
336 * @param config MultipartConfigElement
337 * @param contextTmpDir javax.servlet.context.tempdir
338 */
339 public MultiPartInputStream (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
340 {
341 _in = new ReadLineInputStream(in);
342 _contentType = contentType;
343 _config = config;
344 _contextTmpDir = contextTmpDir;
345 if (_contextTmpDir == null)
346 _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
347
348 if (_config == null)
349 _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
350 }
351
352 /**
353 * Get the already parsed parts.
354 *
355 * @return
356 */
357 public Collection<Part> getParsedParts()
358 {
359 if (_parts == null)
360 return Collections.emptyList();
361
362 Collection<Object> values = _parts.values();
363 List<Part> parts = new ArrayList<Part>();
364 for (Object o: values)
365 {
366 List<Part> asList = LazyList.getList(o, false);
367 parts.addAll(asList);
368 }
369 return parts;
370 }
371
372 /**
373 * Delete any tmp storage for parts, and clear out the parts list.
374 *
375 * @throws MultiException
376 */
377 public void deleteParts ()
378 throws MultiException
379 {
380 Collection<Part> parts = getParsedParts();
381 MultiException err = new MultiException();
382 for (Part p:parts)
383 {
384 try
385 {
386 ((MultiPartInputStream.MultiPart)p).cleanUp();
387 }
388 catch(Exception e)
389 {
390 err.add(e);
391 }
392 }
393 _parts.clear();
394
395 err.ifExceptionThrowMulti();
396 }
397
398
399 /**
400 * Parse, if necessary, the multipart data and return the list of Parts.
401 *
402 * @return
403 * @throws IOException
404 * @throws ServletException
405 */
406 public Collection<Part> getParts()
407 throws IOException, ServletException
408 {
409 parse();
410 Collection<Object> values = _parts.values();
411 List<Part> parts = new ArrayList<Part>();
412 for (Object o: values)
413 {
414 List<Part> asList = LazyList.getList(o, false);
415 parts.addAll(asList);
416 }
417 return parts;
418 }
419
420
421 /**
422 * Get the named Part.
423 *
424 * @param name
425 * @return
426 * @throws IOException
427 * @throws ServletException
428 */
429 public Part getPart(String name)
430 throws IOException, ServletException
431 {
432 parse();
433 return (Part)_parts.getValue(name, 0);
434 }
435
436
437 /**
438 * Parse, if necessary, the multipart stream.
439 *
440 * @throws IOException
441 * @throws ServletException
442 */
443 protected void parse ()
444 throws IOException, ServletException
445 {
446 //have we already parsed the input?
447 if (_parts != null)
448 return;
449
450 //initialize
451 long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
452 _parts = new MultiMap<String>();
453
454 //if its not a multipart request, don't parse it
455 if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
456 return;
457
458 //sort out the location to which to write the files
459
460 if (_config.getLocation() == null)
461 _tmpDir = _contextTmpDir;
462 else if ("".equals(_config.getLocation()))
463 _tmpDir = _contextTmpDir;
464 else
465 {
466 File f = new File (_config.getLocation());
467 if (f.isAbsolute())
468 _tmpDir = f;
469 else
470 _tmpDir = new File (_contextTmpDir, _config.getLocation());
471 }
472
473 if (!_tmpDir.exists())
474 _tmpDir.mkdirs();
475
476 String contentTypeBoundary = "";
477 int bstart = _contentType.indexOf("boundary=");
478 if (bstart >= 0)
479 {
480 int bend = _contentType.indexOf(";", bstart);
481 bend = (bend < 0? _contentType.length(): bend);
482 contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend), true).trim());
483 }
484
485 String boundary="--"+contentTypeBoundary;
486 byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
487
488 // Get first boundary
489 String line = null;
490 try
491 {
492 line=((ReadLineInputStream)_in).readLine();
493 }
494 catch (IOException e)
495 {
496 LOG.warn("Badly formatted multipart request");
497 throw e;
498 }
499
500 if (line == null)
501 throw new IOException("Missing content for multipart request");
502
503 boolean badFormatLogged = false;
504 line=line.trim();
505 while (line != null && !line.equals(boundary))
506 {
507 if (!badFormatLogged)
508 {
509 LOG.warn("Badly formatted multipart request");
510 badFormatLogged = true;
511 }
512 line=((ReadLineInputStream)_in).readLine();
513 line=(line==null?line:line.trim());
514 }
515
516 if (line == null)
517 throw new IOException("Missing initial multi part boundary");
518
519 // Read each part
520 boolean lastPart=false;
521
522 outer:while(!lastPart)
523 {
524 String contentDisposition=null;
525 String contentType=null;
526 String contentTransferEncoding=null;
527
528 MultiMap<String> headers = new MultiMap<String>();
529 while(true)
530 {
531 line=((ReadLineInputStream)_in).readLine();
532
533 //No more input
534 if(line==null)
535 break outer;
536
537 // If blank line, end of part headers
538 if("".equals(line))
539 break;
540
541 total += line.length();
542 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
543 throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
544
545 //get content-disposition and content-type
546 int c=line.indexOf(':',0);
547 if(c>0)
548 {
549 String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
550 String value=line.substring(c+1,line.length()).trim();
551 headers.put(key, value);
552 if (key.equalsIgnoreCase("content-disposition"))
553 contentDisposition=value;
554 if (key.equalsIgnoreCase("content-type"))
555 contentType = value;
556 if(key.equals("content-transfer-encoding"))
557 contentTransferEncoding=value;
558
559 }
560 }
561
562 // Extract content-disposition
563 boolean form_data=false;
564 if(contentDisposition==null)
565 {
566 throw new IOException("Missing content-disposition");
567 }
568
569 QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true);
570 String name=null;
571 String filename=null;
572 while(tok.hasMoreTokens())
573 {
574 String t=tok.nextToken().trim();
575 String tl=t.toLowerCase(Locale.ENGLISH);
576 if(t.startsWith("form-data"))
577 form_data=true;
578 else if(tl.startsWith("name="))
579 name=value(t, true);
580 else if(tl.startsWith("filename="))
581 filename=filenameValue(t);
582 }
583
584 // Check disposition
585 if(!form_data)
586 {
587 continue;
588 }
589 //It is valid for reset and submit buttons to have an empty name.
590 //If no name is supplied, the browser skips sending the info for that field.
591 //However, if you supply the empty string as the name, the browser sends the
592 //field, with name as the empty string. So, only continue this loop if we
593 //have not yet seen a name field.
594 if(name==null)
595 {
596 continue;
597 }
598
599 //Have a new Part
600 MultiPart part = new MultiPart(name, filename);
601 part.setHeaders(headers);
602 part.setContentType(contentType);
603 _parts.add(name, part);
604 part.open();
605
606 InputStream partInput = null;
607 if ("base64".equalsIgnoreCase(contentTransferEncoding))
608 {
609 partInput = new Base64InputStream((ReadLineInputStream)_in);
610 }
611 else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
612 {
613 partInput = new FilterInputStream(_in)
614 {
615 @Override
616 public int read() throws IOException
617 {
618 int c = in.read();
619 if (c >= 0 && c == '=')
620 {
621 int hi = in.read();
622 int lo = in.read();
623 if (hi < 0 || lo < 0)
624 {
625 throw new IOException("Unexpected end to quoted-printable byte");
626 }
627 char[] chars = new char[] { (char)hi, (char)lo };
628 c = Integer.parseInt(new String(chars),16);
629 }
630 return c;
631 }
632 };
633 }
634 else
635 partInput = _in;
636
637 try
638 {
639 int state=-2;
640 int c;
641 boolean cr=false;
642 boolean lf=false;
643
644 // loop for all lines
645 while(true)
646 {
647 int b=0;
648 while((c=(state!=-2)?state:partInput.read())!=-1)
649 {
650 total ++;
651 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
652 throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
653
654 state=-2;
655
656 // look for CR and/or LF
657 if(c==13||c==10)
658 {
659 if(c==13)
660 {
661 partInput.mark(1);
662 int tmp=partInput.read();
663 if (tmp!=10)
664 partInput.reset();
665 else
666 state=tmp;
667 }
668 break;
669 }
670
671 // Look for boundary
672 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
673 {
674 b++;
675 }
676 else
677 {
678 // Got a character not part of the boundary, so we don't have the boundary marker.
679 // Write out as many chars as we matched, then the char we're looking at.
680 if(cr)
681 part.write(13);
682
683 if(lf)
684 part.write(10);
685
686 cr=lf=false;
687 if(b>0)
688 part.write(byteBoundary,0,b);
689
690 b=-1;
691 part.write(c);
692 }
693 }
694
695 // Check for incomplete boundary match, writing out the chars we matched along the way
696 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
697 {
698 if(cr)
699 part.write(13);
700
701 if(lf)
702 part.write(10);
703
704 cr=lf=false;
705 part.write(byteBoundary,0,b);
706 b=-1;
707 }
708
709 // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part.
710 if(b>0||c==-1)
711 {
712
713 if(b==byteBoundary.length)
714 lastPart=true;
715 if(state==10)
716 state=-2;
717 break;
718 }
719
720 // handle CR LF
721 if(cr)
722 part.write(13);
723
724 if(lf)
725 part.write(10);
726
727 cr=(c==13);
728 lf=(c==10||state==10);
729 if(state==10)
730 state=-2;
731 }
732 }
733 finally
734 {
735 part.close();
736 }
737 }
738 if (!lastPart)
739 throw new IOException("Incomplete parts");
740 }
741
742 public void setDeleteOnExit(boolean deleteOnExit)
743 {
744 _deleteOnExit = deleteOnExit;
745 }
746
747
748 public boolean isDeleteOnExit()
749 {
750 return _deleteOnExit;
751 }
752
753
754 /* ------------------------------------------------------------ */
755 private String value(String nameEqualsValue, boolean splitAfterSpace)
756 {
757 /*
758 String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
759 int i=value.indexOf(';');
760 if(i>0)
761 value=value.substring(0,i);
762 if(value.startsWith("\""))
763 {
764 value=value.substring(1,value.indexOf('"',1));
765 }
766 else if (splitAfterSpace)
767 {
768 i=value.indexOf(' ');
769 if(i>0)
770 value=value.substring(0,i);
771 }
772 return value;
773 */
774 int idx = nameEqualsValue.indexOf('=');
775 String value = nameEqualsValue.substring(idx+1).trim();
776 return QuotedStringTokenizer.unquoteOnly(value);
777 }
778
779
780 /* ------------------------------------------------------------ */
781 private String filenameValue(String nameEqualsValue)
782 {
783 int idx = nameEqualsValue.indexOf('=');
784 String value = nameEqualsValue.substring(idx+1).trim();
785
786 if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
787 {
788 //incorrectly escaped IE filenames that have the whole path
789 //we just strip any leading & trailing quotes and leave it as is
790 char first=value.charAt(0);
791 if (first=='"' || first=='\'')
792 value=value.substring(1);
793 char last=value.charAt(value.length()-1);
794 if (last=='"' || last=='\'')
795 value = value.substring(0,value.length()-1);
796
797 return value;
798 }
799 else
800 //unquote the string, but allow any backslashes that don't
801 //form a valid escape sequence to remain as many browsers
802 //even on *nix systems will not escape a filename containing
803 //backslashes
804 return QuotedStringTokenizer.unquoteOnly(value, true);
805 }
806
807 private static class Base64InputStream extends InputStream
808 {
809 ReadLineInputStream _in;
810 String _line;
811 byte[] _buffer;
812 int _pos;
813
814
815 public Base64InputStream(ReadLineInputStream rlis)
816 {
817 _in = rlis;
818 }
819
820 @Override
821 public int read() throws IOException
822 {
823 if (_buffer==null || _pos>= _buffer.length)
824 {
825 //Any CR and LF will be consumed by the readLine() call.
826 //We need to put them back into the bytes returned from this
827 //method because the parsing of the multipart content uses them
828 //as markers to determine when we've reached the end of a part.
829 _line = _in.readLine();
830 if (_line==null)
831 return -1; //nothing left
832 if (_line.startsWith("--"))
833 _buffer=(_line+"\r\n").getBytes(); //boundary marking end of part
834 else if (_line.length()==0)
835 _buffer="\r\n".getBytes(); //blank line
836 else
837 {
838 ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2);
839 B64Code.decode(_line, baos);
840 baos.write(13);
841 baos.write(10);
842 _buffer = baos.toByteArray();
843 }
844
845 _pos=0;
846 }
847
848 return _buffer[_pos++];
849 }
850 }
851}