View Javadoc

1   // ========================================================================
2   // Copyright (c) 2006-2010 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.util;
15  
16  import java.io.BufferedInputStream;
17  import java.io.BufferedOutputStream;
18  import java.io.BufferedReader;
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.FilterInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.InputStreamReader;
29  import java.io.OutputStream;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.StringTokenizer;
36  
37  import javax.servlet.MultipartConfigElement;
38  import javax.servlet.ServletException;
39  import javax.servlet.http.Part;
40  
41  
42  
43  /**
44   * MultiPartInputStream
45   *
46   * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
47   */
48  public class MultiPartInputStream
49  {
50      public static final MultipartConfigElement  __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
51      protected InputStream _in;
52      protected MultipartConfigElement _config;
53      protected String _contentType;
54      protected MultiMap<String> _parts;
55      protected File _tmpDir;
56      protected File _contextTmpDir;
57   
58      
59      
60      
61      public class MultiPart implements Part
62      {
63          protected String _name;
64          protected String _filename;
65          protected File _file;
66          protected OutputStream _out;
67          protected String _contentType;
68          protected MultiMap<String> _headers;
69          protected long _size = 0;
70  
71          public MultiPart (String name, String filename) 
72          throws IOException
73          {
74              _name = name;
75              _filename = filename;
76          }
77  
78          protected void setContentType (String contentType)
79          {
80              _contentType = contentType;
81          }
82          
83          
84          protected void open() 
85          throws FileNotFoundException, IOException
86          {
87              //We will either be writing to a file, if it has a filename on the content-disposition
88              //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
89              //will need to change to write to a file.           
90              if (_filename != null && _filename.trim().length() > 0)
91              {
92                  createFile();            
93              }
94              else
95              {
96                  //Write to a buffer in memory until we discover we've exceed the 
97                  //MultipartConfig fileSizeThreshold
98                  _out = new ByteArrayOutputStream();
99              }
100         }
101         
102         protected void close() 
103         throws IOException
104         {
105             _out.close();
106         }
107         
108       
109         protected void write (int b)
110         throws IOException, ServletException
111         {      
112             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStream.this._config.getMaxFileSize())
113                 throw new ServletException ("Multipart Mime part "+_name+" exceeds max filesize");
114             
115             if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
116                 createFile();
117             _out.write(b);   
118             _size ++;
119         }
120         
121         protected void write (byte[] bytes, int offset, int length) 
122         throws IOException, ServletException
123         { 
124             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStream.this._config.getMaxFileSize())
125                 throw new ServletException ("Multipart Mime part "+_name+" exceeds max filesize");
126             
127             if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
128                 createFile();
129             
130             _out.write(bytes, offset, length);
131             _size += length;
132         }
133         
134         protected void createFile ()
135         throws IOException
136         {
137             _file = File.createTempFile("MultiPart", "", MultiPartInputStream.this._tmpDir);
138             FileOutputStream fos = new FileOutputStream(_file);
139             BufferedOutputStream bos = new BufferedOutputStream(fos);
140             
141             if (_size > 0 && _out != null)
142             {
143                 //already written some bytes, so need to copy them into the file
144                 _out.flush();
145                 ((ByteArrayOutputStream)_out).writeTo(bos);
146                 _out.close();
147             }
148             _out = bos;
149         }
150         
151 
152         
153         protected void setHeaders(MultiMap<String> headers)
154         {
155             _headers = headers;
156         }
157         
158         /** 
159          * @see javax.servlet.http.Part#getContentType()
160          */
161         public String getContentType()
162         {
163             return _contentType;
164         }
165 
166         /** 
167          * @see javax.servlet.http.Part#getHeader(java.lang.String)
168          */
169         public String getHeader(String name)
170         {
171             return (String)_headers.getValue(name, 0);
172         }
173 
174         /** 
175          * @see javax.servlet.http.Part#getHeaderNames()
176          */
177         public Collection<String> getHeaderNames()
178         {
179             return _headers.keySet();
180         }
181 
182         /** 
183          * @see javax.servlet.http.Part#getHeaders(java.lang.String)
184          */
185         public Collection<String> getHeaders(String name)
186         {
187            return _headers.getValues(name);
188         }
189 
190         /** 
191          * @see javax.servlet.http.Part#getInputStream()
192          */
193         public InputStream getInputStream() throws IOException
194         {
195            if (_file != null)
196            {
197                return new BufferedInputStream (new FileInputStream(_file));
198            }
199            else
200            {
201                //part content is in a ByteArrayOutputStream
202                return new ByteArrayInputStream(((ByteArrayOutputStream)_out).toByteArray());
203            }
204         }
205 
206         /** 
207          * @see javax.servlet.http.Part#getName()
208          */
209         public String getName()
210         {
211            return _name;
212         }
213 
214         /** 
215          * @see javax.servlet.http.Part#getSize()
216          */
217         public long getSize()
218         {
219             return _size;
220         }
221 
222         /** 
223          * @see javax.servlet.http.Part#write(java.lang.String)
224          */
225         public void write(String fileName) throws IOException
226         {
227             if (_file == null)
228             {
229                 //part data is only in the ByteArrayOutputStream and never been written to disk
230                 _file = new File (_tmpDir, fileName);
231                 BufferedOutputStream bos = null;
232                 try
233                 {
234                     bos = new BufferedOutputStream(new FileOutputStream(_file));
235                     ((ByteArrayOutputStream)_out).writeTo(bos);
236                     bos.flush();
237                 }
238                 finally
239                 {
240                     if (bos != null)
241                         bos.close();
242                 }
243             }
244             else
245             {
246                 //the part data is already written to a temporary file, just rename it
247                 _file.renameTo(new File(_tmpDir, fileName));
248             }
249         }
250         
251         /** 
252          * @see javax.servlet.http.Part#delete()
253          */
254         public void delete() throws IOException
255         {
256             if (_file != null)
257                 _file.delete();     
258         }
259         
260         
261         /**
262          * Get the file, if any, the data has been written to.
263          * @return
264          */
265         public File getFile ()
266         {
267             return _file;
268         }  
269         
270         
271         /**
272          * Get the filename from the content-disposition.
273          * @return null or the filename
274          */
275         public String getContentDispositionFilename ()
276         {
277             return _filename;
278         }
279     }
280     
281     
282     
283     
284     /**
285      * @param in Request input stream 
286      * @param contentType Content-Type header
287      * @param config MultipartConfigElement 
288      * @param contextTmpDir javax.servlet.context.tempdir
289      */
290     public MultiPartInputStream (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
291     {
292         _in = new BufferedInputStream(in);
293        _contentType = contentType;
294        _config = config;
295        _contextTmpDir = contextTmpDir;
296        if (_contextTmpDir == null)
297            _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
298        if (_config == null)
299            _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
300     }
301 
302     
303    
304     public Collection<Part> getParts()
305     throws IOException, ServletException
306     {
307         parse();
308         Collection<Object> values = _parts.values();
309         List<Part> parts = new ArrayList<Part>();
310         for (Object o: values)
311         {
312             List<Part> asList = LazyList.getList(o, false);
313             parts.addAll(asList);
314         }
315         return parts;
316     }
317     
318     
319     public Part getPart(String name)
320     throws IOException, ServletException
321     {
322         parse();
323         return (Part)_parts.getValue(name, 0);
324     }
325     
326     
327     protected void parse ()
328     throws IOException, ServletException
329     {
330         //have we already parsed the input?
331         if (_parts != null)
332             return;
333         
334         //initialize
335         long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize              
336         _parts = new MultiMap<String>();
337 
338         //if its not a multipart request, don't parse it
339         if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
340             return;
341  
342         //sort out the location to which to write the files
343         
344         if (_config.getLocation() == null)
345             _tmpDir = _contextTmpDir;
346         else if ("".equals(_config.getLocation()))
347             _tmpDir = _contextTmpDir;
348         else
349         {
350             File f = new File (_config.getLocation());
351             if (f.isAbsolute())
352                 _tmpDir = f;
353             else
354                 _tmpDir = new File (_contextTmpDir, _config.getLocation());
355         }
356       
357         if (!_tmpDir.exists())
358             _tmpDir.mkdirs();
359 
360         String boundary="--"+QuotedStringTokenizer.unquote(value(_contentType.substring(_contentType.indexOf("boundary="))).trim());
361         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
362 
363         // Get first boundary
364         byte[] bytes=TypeUtil.readLine(_in);
365         String line=bytes==null?null:new String(bytes,"UTF-8");
366         if(line==null || !line.equals(boundary))
367         {
368             throw new IOException("Missing initial multi part boundary");
369         }
370 
371         // Read each part
372         boolean lastPart=false;
373         String contentDisposition=null;
374         String contentType=null;
375         String contentTransferEncoding=null;
376         outer:while(!lastPart)
377         {
378             MultiMap<String> headers = new MultiMap<String>();
379             while(true)
380             {
381                 bytes=TypeUtil.readLine(_in);
382                 if(bytes==null)
383                     break outer;
384 
385                 // If blank line, end of part headers
386                 if(bytes.length==0)
387                     break;
388                 
389                 total += bytes.length;
390                 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
391                     throw new ServletException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
392                 
393                 line=new String(bytes,"UTF-8");
394 
395                 //get content-disposition and content-type
396                 int c=line.indexOf(':',0);
397                 if(c>0)
398                 {
399                     String key=line.substring(0,c).trim().toLowerCase();
400                     String value=line.substring(c+1,line.length()).trim();
401                     headers.put(key, value);
402                     if (key.equalsIgnoreCase("content-disposition"))
403                         contentDisposition=value;
404                     if (key.equalsIgnoreCase("content-type"))
405                         contentType = value;
406                     if(key.equals("content-transfer-encoding"))
407                         contentTransferEncoding=value;
408 
409                 }
410             }
411 
412             // Extract content-disposition
413             boolean form_data=false;
414             if(contentDisposition==null)
415             {
416                 throw new IOException("Missing content-disposition");
417             }
418 
419             QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";");
420             String name=null;
421             String filename=null;
422             while(tok.hasMoreTokens())
423             {
424                 String t=tok.nextToken().trim();
425                 String tl=t.toLowerCase();
426                 if(t.startsWith("form-data"))
427                     form_data=true;
428                 else if(tl.startsWith("name="))
429                     name=value(t);
430                 else if(tl.startsWith("filename="))
431                     filename=value(t);
432             }
433 
434             // Check disposition
435             if(!form_data)
436             {
437                 continue;
438             }
439             //It is valid for reset and submit buttons to have an empty name.
440             //If no name is supplied, the browser skips sending the info for that field.
441             //However, if you supply the empty string as the name, the browser sends the
442             //field, with name as the empty string. So, only continue this loop if we
443             //have not yet seen a name field.
444             if(name==null)
445             {
446                 continue;
447             }
448 
449             if ("base64".equalsIgnoreCase(contentTransferEncoding))
450             {
451                 _in = new Base64InputStream(_in);
452             }
453             else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
454             {
455                 _in = new FilterInputStream(_in)
456                 {
457                     @Override
458                     public int read() throws IOException
459                     {
460                         int c = in.read();
461                         if (c >= 0 && c == '=')
462                         {
463                             int hi = in.read();
464                             int lo = in.read();
465                             if (hi < 0 || lo < 0)
466                             {
467                                 throw new IOException("Unexpected end to quoted-printable byte");
468                             }
469                             char[] chars = new char[] { (char)hi, (char)lo };
470                             c = Integer.parseInt(new String(chars),16);
471                         }
472                         return c;
473                     }
474                 };
475             }
476 
477             
478             
479             //Have a new Part
480             MultiPart part = new MultiPart(name, filename);
481             part.setHeaders(headers);
482             part.setContentType(contentType);
483             _parts.add(name, part);
484 
485             part.open();
486 
487             try
488             { 
489                 int state=-2;
490                 int c;
491                 boolean cr=false;
492                 boolean lf=false;
493 
494                 // loop for all lines`
495                 while(true)
496                 {
497                     int b=0;
498                     while((c=(state!=-2)?state:_in.read())!=-1)
499                     {
500                         total ++;
501                         if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
502                             throw new ServletException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
503                         
504                         state=-2;
505                         // look for CR and/or LF
506                         if(c==13||c==10)
507                         {
508                             if(c==13)
509                                 state=_in.read();
510                             break;
511                         }
512                         // look for boundary
513                         if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
514                             b++;
515                         else
516                         {
517                             // this is not a boundary
518                             if(cr)
519                                 part.write(13);
520                     
521                             if(lf)
522                                 part.write(10); 
523                             
524                             cr=lf=false;
525                             if(b>0)
526                                 part.write(byteBoundary,0,b);
527                               
528                             b=-1;
529                             part.write(c);
530                         }
531                     }
532                     // check partial boundary
533                     if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
534                     {
535                         if(cr)
536                             part.write(13);
537 
538                         if(lf)
539                             part.write(10);
540 
541                         cr=lf=false;
542                         part.write(byteBoundary,0,b);
543                         b=-1;
544                     }
545                     // boundary match
546                     if(b>0||c==-1)
547                     {
548                         if(b==byteBoundary.length)
549                             lastPart=true;
550                         if(state==10)
551                             state=-2;
552                         break;
553                     }
554                     // handle CR LF
555                     if(cr)
556                         part.write(13); 
557 
558                     if(lf)
559                         part.write(10);
560 
561                     cr=(c==13);
562                     lf=(c==10||state==10);
563                     if(state==10)
564                         state=-2;
565                 }
566             }
567             finally
568             {
569  
570                 part.close();
571             }
572         }
573     }
574     
575     
576     /* ------------------------------------------------------------ */
577     private String value(String nameEqualsValue)
578     {
579         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
580         int i=value.indexOf(';');
581         if(i>0)
582             value=value.substring(0,i);
583         if(value.startsWith("\""))
584         {
585             value=value.substring(1,value.indexOf('"',1));
586         }
587         else
588         {
589             i=value.indexOf(' ');
590             if(i>0)
591                 value=value.substring(0,i);
592         }
593         return value;
594     }
595     
596     private static class Base64InputStream extends InputStream
597     {
598         BufferedReader _in;
599         String _line;
600         byte[] _buffer;
601         int _pos;
602 
603         public Base64InputStream (InputStream in)
604         {
605             _in = new BufferedReader(new InputStreamReader(in));
606         }
607 
608         @Override
609         public int read() throws IOException
610         {
611             if (_buffer==null || _pos>= _buffer.length)
612             {
613                 _line = _in.readLine();
614                 if (_line==null)
615                     return -1;
616                 if (_line.startsWith("--"))
617                     _buffer=(_line+"\r\n").getBytes();
618                 else if (_line.length()==0)
619                     _buffer="\r\n".getBytes();
620                 else
621                     _buffer=B64Code.decode(_line);
622 
623                 _pos=0;
624             }
625             return _buffer[_pos++];
626         }
627     }
628 }