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         
220         /** 
221          * @see javax.servlet.http.Part#getSubmittedFileName()
222          */
223         @Override
224         public String getSubmittedFileName()
225         {
226             return getContentDispositionFilename();
227         }
228 
229         public byte[] getBytes()
230         {
231             if (_bout!=null)
232                 return _bout.toByteArray();
233             return null;
234         }
235 
236         /**
237          * @see javax.servlet.http.Part#getName()
238          */
239         public String getName()
240         {
241            return _name;
242         }
243 
244         /**
245          * @see javax.servlet.http.Part#getSize()
246          */
247         public long getSize()
248         {
249             return _size;         
250         }
251 
252         /**
253          * @see javax.servlet.http.Part#write(java.lang.String)
254          */
255         public void write(String fileName) throws IOException
256         {
257             if (_file == null)
258             {
259                 _temporary = false;
260                 
261                 //part data is only in the ByteArrayOutputStream and never been written to disk
262                 _file = new File (_tmpDir, fileName);
263 
264                 BufferedOutputStream bos = null;
265                 try
266                 {
267                     bos = new BufferedOutputStream(new FileOutputStream(_file));
268                     _bout.writeTo(bos);
269                     bos.flush();
270                 }
271                 finally
272                 {
273                     if (bos != null)
274                         bos.close();
275                     _bout = null;
276                 }
277             }
278             else
279             {
280                 //the part data is already written to a temporary file, just rename it
281                 _temporary = false;
282                 
283                 File f = new File(_tmpDir, fileName);
284                 if (_file.renameTo(f))
285                     _file = f;
286             }
287         }
288 
289         /**
290          * Remove the file, whether or not Part.write() was called on it
291          * (ie no longer temporary)
292          * @see javax.servlet.http.Part#delete()
293          */
294         public void delete() throws IOException
295         {
296             if (_file != null && _file.exists())
297                 _file.delete();     
298         }
299         
300         /**
301          * Only remove tmp files.
302          * 
303          * @throws IOException
304          */
305         public void cleanUp() throws IOException
306         {
307             if (_temporary && _file != null && _file.exists())
308                 _file.delete();
309         }
310 
311 
312         /**
313          * Get the file, if any, the data has been written to.
314          */
315         public File getFile ()
316         {
317             return _file;
318         }
319 
320 
321         /**
322          * Get the filename from the content-disposition.
323          * @return null or the filename
324          */
325         public String getContentDispositionFilename ()
326         {
327             return _filename;
328         }
329     }
330 
331 
332 
333 
334     /**
335      * @param in Request input stream
336      * @param contentType Content-Type header
337      * @param config MultipartConfigElement
338      * @param contextTmpDir javax.servlet.context.tempdir
339      */
340     public MultiPartInputStreamParser (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
341     {
342         _in = new ReadLineInputStream(in);
343        _contentType = contentType;
344        _config = config;
345        _contextTmpDir = contextTmpDir;
346        if (_contextTmpDir == null)
347            _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
348        
349        if (_config == null)
350            _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
351     }
352 
353     /**
354      * Get the already parsed parts.
355      */
356     public Collection<Part> getParsedParts()
357     {
358         if (_parts == null)
359             return Collections.emptyList();
360 
361         Collection<Object> values = _parts.values();
362         List<Part> parts = new ArrayList<Part>();
363         for (Object o: values)
364         {
365             List<Part> asList = LazyList.getList(o, false);
366             parts.addAll(asList);
367         }
368         return parts;
369     }
370 
371     /**
372      * Delete any tmp storage for parts, and clear out the parts list.
373      * 
374      * @throws MultiException
375      */
376     public void deleteParts ()
377     throws MultiException
378     {
379         Collection<Part> parts = getParsedParts();
380         MultiException err = new MultiException();
381         for (Part p:parts)
382         {
383             try
384             {
385                 ((MultiPartInputStreamParser.MultiPart)p).cleanUp();
386             } 
387             catch(Exception e)
388             {     
389                 err.add(e); 
390             }
391         }
392         _parts.clear();
393         
394         err.ifExceptionThrowMulti();
395     }
396 
397    
398     /**
399      * Parse, if necessary, the multipart data and return the list of Parts.
400      * 
401      * @throws IOException
402      * @throws ServletException
403      */
404     public Collection<Part> getParts()
405     throws IOException, ServletException
406     {
407         parse();
408         Collection<Object> values = _parts.values();
409         List<Part> parts = new ArrayList<Part>();
410         for (Object o: values)
411         {
412             List<Part> asList = LazyList.getList(o, false);
413             parts.addAll(asList);
414         }
415         return parts;
416     }
417 
418 
419     /**
420      * Get the named Part.
421      * 
422      * @param name
423      * @throws IOException
424      * @throws ServletException
425      */
426     public Part getPart(String name)
427     throws IOException, ServletException
428     {
429         parse();
430         return (Part)_parts.getValue(name, 0);
431     }
432 
433 
434     /**
435      * Parse, if necessary, the multipart stream.
436      * 
437      * @throws IOException
438      * @throws ServletException
439      */
440     protected void parse ()
441     throws IOException, ServletException
442     {
443         //have we already parsed the input?
444         if (_parts != null)
445             return;
446 
447         //initialize
448         long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
449         _parts = new MultiMap();
450 
451         //if its not a multipart request, don't parse it
452         if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
453             return;
454 
455         //sort out the location to which to write the files
456 
457         if (_config.getLocation() == null)
458             _tmpDir = _contextTmpDir;
459         else if ("".equals(_config.getLocation()))
460             _tmpDir = _contextTmpDir;
461         else
462         {
463             File f = new File (_config.getLocation());
464             if (f.isAbsolute())
465                 _tmpDir = f;
466             else
467                 _tmpDir = new File (_contextTmpDir, _config.getLocation());
468         }
469 
470         if (!_tmpDir.exists())
471             _tmpDir.mkdirs();
472 
473         String contentTypeBoundary = "";
474         int bstart = _contentType.indexOf("boundary=");
475         if (bstart >= 0)
476         {
477             int bend = _contentType.indexOf(";", bstart);
478             bend = (bend < 0? _contentType.length(): bend);
479             contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
480         }
481         
482         String boundary="--"+contentTypeBoundary;
483         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
484 
485         // Get first boundary
486         String line = null;
487         try
488         {
489             line=((ReadLineInputStream)_in).readLine();  
490         }
491         catch (IOException e)
492         {
493             LOG.warn("Badly formatted multipart request");
494             throw e;
495         }
496         
497         if (line == null)
498             throw new IOException("Missing content for multipart request");
499         
500         boolean badFormatLogged = false;
501         line=line.trim();
502         while (line != null && !line.equals(boundary))
503         {
504             if (!badFormatLogged)
505             {
506                 LOG.warn("Badly formatted multipart request");
507                 badFormatLogged = true;
508             }
509             line=((ReadLineInputStream)_in).readLine();
510             line=(line==null?line:line.trim());
511         }
512 
513         if (line == null)
514             throw new IOException("Missing initial multi part boundary");
515 
516         // Read each part
517         boolean lastPart=false;
518 
519         outer:while(!lastPart)
520         {
521             String contentDisposition=null;
522             String contentType=null;
523             String contentTransferEncoding=null;
524             
525             MultiMap headers = new MultiMap();
526             while(true)
527             {
528                 line=((ReadLineInputStream)_in).readLine();
529                 
530                 //No more input
531                 if(line==null)
532                     break outer;
533                 
534                 //end of headers:
535                 if("".equals(line))
536                     break;
537            
538                 total += line.length();
539                 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
540                     throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
541 
542                 //get content-disposition and content-type
543                 int c=line.indexOf(':',0);
544                 if(c>0)
545                 {
546                     String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
547                     String value=line.substring(c+1,line.length()).trim();
548                     headers.put(key, value);
549                     if (key.equalsIgnoreCase("content-disposition"))
550                         contentDisposition=value;
551                     if (key.equalsIgnoreCase("content-type"))
552                         contentType = value;
553                     if(key.equals("content-transfer-encoding"))
554                         contentTransferEncoding=value;
555                 }
556             }
557 
558             // Extract content-disposition
559             boolean form_data=false;
560             if(contentDisposition==null)
561             {
562                 throw new IOException("Missing content-disposition");
563             }
564 
565             QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true);
566             String name=null;
567             String filename=null;
568             while(tok.hasMoreTokens())
569             {
570                 String t=tok.nextToken().trim();
571                 String tl=t.toLowerCase(Locale.ENGLISH);
572                 if(t.startsWith("form-data"))
573                     form_data=true;
574                 else if(tl.startsWith("name="))
575                     name=value(t);
576                 else if(tl.startsWith("filename="))
577                     filename=filenameValue(t);
578             }
579 
580             // Check disposition
581             if(!form_data)
582             {
583                 continue;
584             }
585             //It is valid for reset and submit buttons to have an empty name.
586             //If no name is supplied, the browser skips sending the info for that field.
587             //However, if you supply the empty string as the name, the browser sends the
588             //field, with name as the empty string. So, only continue this loop if we
589             //have not yet seen a name field.
590             if(name==null)
591             {
592                 continue;
593             }
594 
595             //Have a new Part
596             MultiPart part = new MultiPart(name, filename);
597             part.setHeaders(headers);
598             part.setContentType(contentType);
599             _parts.add(name, part);
600             part.open();
601             
602             InputStream partInput = null;
603             if ("base64".equalsIgnoreCase(contentTransferEncoding))
604             {
605                 partInput = new Base64InputStream((ReadLineInputStream)_in);
606             }
607             else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
608             {
609                 partInput = new FilterInputStream(_in)
610                 {
611                     @Override
612                     public int read() throws IOException
613                     {
614                         int c = in.read();
615                         if (c >= 0 && c == '=')
616                         {
617                             int hi = in.read();
618                             int lo = in.read();
619                             if (hi < 0 || lo < 0)
620                             {
621                                 throw new IOException("Unexpected end to quoted-printable byte");
622                             }
623                             char[] chars = new char[] { (char)hi, (char)lo };
624                             c = Integer.parseInt(new String(chars),16);
625                         }
626                         return c;
627                     }
628                 };
629             }
630             else
631                 partInput = _in;
632 
633             
634             try
635             {
636                 int state=-2;
637                 int c;
638                 boolean cr=false;
639                 boolean lf=false;
640 
641                 // loop for all lines
642                 while(true)
643                 {
644                     int b=0;
645                     while((c=(state!=-2)?state:partInput.read())!=-1)
646                     {
647                         total ++;
648                         if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
649                             throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
650 
651                         state=-2;
652                         
653                         // look for CR and/or LF
654                         if(c==13||c==10)
655                         {
656                             if(c==13)
657                             {
658                                 partInput.mark(1);
659                                 int tmp=partInput.read();
660                                 if (tmp!=10)
661                                     partInput.reset();
662                                 else
663                                     state=tmp;
664                             }
665                             break;
666                         }
667                         
668                         // Look for boundary
669                         if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
670                         {
671                             b++;
672                         }
673                         else
674                         {
675                             // Got a character not part of the boundary, so we don't have the boundary marker.
676                             // Write out as many chars as we matched, then the char we're looking at.
677                             if(cr)
678                                 part.write(13);
679 
680                             if(lf)
681                                 part.write(10);
682 
683                             cr=lf=false;
684                             if(b>0)
685                                 part.write(byteBoundary,0,b);
686 
687                             b=-1;
688                             part.write(c);
689                         }
690                     }
691                     
692                     // Check for incomplete boundary match, writing out the chars we matched along the way
693                     if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
694                     {
695                         if(cr)
696                             part.write(13);
697 
698                         if(lf)
699                             part.write(10);
700 
701                         cr=lf=false;
702                         part.write(byteBoundary,0,b);
703                         b=-1;
704                     }
705                     
706                     // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part.
707                     if(b>0||c==-1)
708                     {
709                        
710                         if(b==byteBoundary.length)
711                             lastPart=true;
712                         if(state==10)
713                             state=-2;
714                         break;
715                     }
716                     
717                     // handle CR LF
718                     if(cr)
719                         part.write(13);
720 
721                     if(lf)
722                         part.write(10);
723 
724                     cr=(c==13);
725                     lf=(c==10||state==10);
726                     if(state==10)
727                         state=-2;
728                 }
729             }
730             finally
731             {
732 
733                 part.close();
734             }
735         }
736         if (!lastPart)
737             throw new IOException("Incomplete parts");
738     }
739     
740     public void setDeleteOnExit(boolean deleteOnExit)
741     {
742         _deleteOnExit = deleteOnExit;
743     }
744 
745 
746     public boolean isDeleteOnExit()
747     {
748         return _deleteOnExit;
749     }
750 
751 
752     /* ------------------------------------------------------------ */
753     private String value(String nameEqualsValue)
754     {
755         int idx = nameEqualsValue.indexOf('=');
756         String value = nameEqualsValue.substring(idx+1).trim();
757         return QuotedStringTokenizer.unquoteOnly(value);
758     }
759     
760     
761     /* ------------------------------------------------------------ */
762     private String filenameValue(String nameEqualsValue)
763     {
764         int idx = nameEqualsValue.indexOf('=');
765         String value = nameEqualsValue.substring(idx+1).trim();
766 
767         if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
768         {
769             //incorrectly escaped IE filenames that have the whole path
770             //we just strip any leading & trailing quotes and leave it as is
771             char first=value.charAt(0);
772             if (first=='"' || first=='\'')
773                 value=value.substring(1);
774             char last=value.charAt(value.length()-1);
775             if (last=='"' || last=='\'')
776                 value = value.substring(0,value.length()-1);
777 
778             return value;
779         }
780         else
781             //unquote the string, but allow any backslashes that don't
782             //form a valid escape sequence to remain as many browsers
783             //even on *nix systems will not escape a filename containing
784             //backslashes
785             return QuotedStringTokenizer.unquoteOnly(value, true);
786     }
787 
788     
789 
790     private static class Base64InputStream extends InputStream
791     {
792         ReadLineInputStream _in;
793         String _line;
794         byte[] _buffer;
795         int _pos;
796 
797     
798         public Base64InputStream(ReadLineInputStream rlis)
799         {
800             _in = rlis;
801         }
802 
803         @Override
804         public int read() throws IOException
805         {
806             if (_buffer==null || _pos>= _buffer.length)
807             {
808                 //Any CR and LF will be consumed by the readLine() call.
809                 //We need to put them back into the bytes returned from this
810                 //method because the parsing of the multipart content uses them
811                 //as markers to determine when we've reached the end of a part.
812                 _line = _in.readLine(); 
813                 if (_line==null)
814                     return -1;  //nothing left
815                 if (_line.startsWith("--"))
816                     _buffer=(_line+"\r\n").getBytes(); //boundary marking end of part
817                 else if (_line.length()==0)
818                     _buffer="\r\n".getBytes(); //blank line
819                 else
820                 {
821                     ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2);
822                     B64Code.decode(_line, baos);
823                     baos.write(13);
824                     baos.write(10);
825                     _buffer = baos.toByteArray();
826                 }
827 
828                 _pos=0;
829             }
830             
831             return _buffer[_pos++];
832         }
833     }
834 }