View Javadoc

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