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