View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.transport.http.multipart;
8   // ========================================================================
9   // Copyright (c) 2006-2010 Mort Bay Consulting Pty. Ltd.
10  // ------------------------------------------------------------------------
11  // All rights reserved. This program and the accompanying materials
12  // are made available under the terms of the Eclipse Public License v1.0
13  // and Apache License v2.0 which accompanies this distribution.
14  // The Eclipse Public License is available at
15  // http://www.eclipse.org/legal/epl-v10.html
16  // The Apache License v2.0 is available at
17  // http://www.opensource.org/licenses/apache2.0.php
18  // You may elect to redistribute this code under either of these licenses.
19  // ========================================================================
20  
21  
22  import org.mule.model.streaming.DeleteOnCloseFileInputStream;
23  
24  import java.io.BufferedInputStream;
25  import java.io.BufferedOutputStream;
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.File;
29  import java.io.FileNotFoundException;
30  import java.io.FileOutputStream;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.OutputStream;
34  import java.util.Collection;
35  import java.util.HashMap;
36  import java.util.Map;
37  import java.util.StringTokenizer;
38  
39  import javax.servlet.ServletException;
40  
41  import org.apache.commons.io.input.AutoCloseInputStream;
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 MultipartConfiguration __DEFAULT_MULTIPART_CONFIG = new MultipartConfiguration(System.getProperty("java.io.tmpdir"));
51      protected InputStream _in;
52      protected MultipartConfiguration _config;
53      protected String _contentType;
54      protected MultiMap _map;
55      protected Map<String, Part> _parts;
56      protected File _tmpDir;
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
111         {
112             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStream.this._config.getMaxFileSize())
113                 throw new IOException ("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
123         {
124             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStream.this._config.getMaxFileSize())
125                 throw new IOException ("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 Part#getContentType()
160          */
161         public String getContentType()
162         {
163             return _contentType;
164         }
165 
166         /**
167          * @see 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 Part#getHeaderNames()
176          */
177         public Collection<String> getHeaderNames()
178         {
179             return _headers.keySet();
180         }
181 
182         /**
183          * @see Part#getHeaders(java.lang.String)
184          */
185         public Collection<String> getHeaders(String name)
186         {
187            return _headers.getValues(name);
188         }
189 
190         /**
191          * @see Part#getInputStream()
192          */
193         public InputStream getInputStream() throws IOException
194         {
195            if (_file != null)
196            {
197                // Automatically close and delete the temp file when end of input has been reached (MULE-6732).
198                return new BufferedInputStream(new AutoCloseInputStream(new DeleteOnCloseFileInputStream(_file)));
199            }
200            else
201            {
202                //part content is in a ByteArrayOutputStream
203                return new ByteArrayInputStream(((ByteArrayOutputStream)_out).toByteArray());
204            }
205         }
206 
207         /**
208          * @see Part#getName()
209          */
210         public String getName()
211         {
212            return _name;
213         }
214 
215         /**
216          * @see Part#getSize()
217          */
218         public long getSize()
219         {
220             return _size;
221         }
222 
223         /**
224          * @see Part#write(java.lang.String)
225          */
226         public void write(String fileName) throws IOException
227         {
228             if (_file == null)
229             {
230                 //part data is only in the ByteArrayOutputStream and never been written to disk
231                 _file = new File (_tmpDir, fileName);
232                 BufferedOutputStream bos = null;
233                 try
234                 {
235                     bos = new BufferedOutputStream(new FileOutputStream(_file));
236                     ((ByteArrayOutputStream)_out).writeTo(bos);
237                     bos.flush();
238                 }
239                 finally
240                 {
241                     if (bos != null)
242                         bos.close();
243                 }
244             }
245             else
246             {
247                 //the part data is already written to a temporary file, just rename it
248                 _file.renameTo(new File(_tmpDir, fileName));
249             }
250         }
251 
252         /**
253          * @see Part#delete()
254          */
255         public void delete() throws IOException
256         {
257             if (_file != null)
258                 _file.delete();
259         }
260 
261 
262         /**
263          * Get the file, if any, the data has been written to.
264          * @return
265          */
266         public File getFile ()
267         {
268             return _file;
269         }
270 
271 
272         /**
273          * Get the filename from the content-disposition.
274          * @return null or the filename
275          */
276         public String getContentDispositionFilename ()
277         {
278             return _filename;
279         }
280     }
281 
282 
283 
284 
285     /**
286      * @param in Request input stream
287      * @param contentType Content-Type header
288      * @param config MultipartConfiguration
289      */
290     public MultiPartInputStream(InputStream in, String contentType, MultipartConfiguration config)
291     {
292         _in = new BufferedInputStream(in);
293        _contentType = contentType;
294        _config = config;
295        if (_config == null)
296            _config = __DEFAULT_MULTIPART_CONFIG;
297     }
298 
299 
300 
301     public Collection<Part> getParts()
302     throws IOException
303     {
304         parse();
305         return _parts.values();
306     }
307 
308 
309     public Part getPart(String name)
310     throws IOException, ServletException
311     {
312         parse();
313         return _parts.get(name);
314     }
315 
316 
317     public MultiMap getMap ()
318     throws IOException, ServletException
319     {
320         parse();
321         return _map;
322     }
323 
324 
325     protected void parse ()
326     throws IOException
327     {
328         //have we already parsed the input?
329         if (_parts != null)
330             return;
331 
332         //initialize
333         long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfiguration._maxRequestSize
334         _parts = new HashMap<String, Part>();
335 
336         //if its not a multipart request, don't parse it
337         if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
338             return;
339 
340         //sort out the location to which to write the files
341         String location = __DEFAULT_MULTIPART_CONFIG.getLocation();
342         location = ("".equals(_config.getLocation())? location : _config.getLocation());
343 
344         _tmpDir = new File(location);
345         if (!_tmpDir.exists())
346             _tmpDir.mkdirs();
347 
348 
349         String boundary="--"+value(_contentType.substring(_contentType.indexOf("boundary=")));
350         byte[] byteBoundary=(boundary+"--").getBytes("ISO-8859-1");
351 
352         // Get first boundary
353         byte[] bytes=readLine(_in);
354         String line=bytes==null?null:new String(bytes,"UTF-8");
355         if(line==null || !line.equals(boundary))
356         {
357             throw new IOException("Missing initial multi part boundary");
358         }
359 
360         // Read each part
361         boolean lastPart=false;
362         String contentDisposition=null;
363         String contentType=null;
364         outer:while(!lastPart)
365         {
366             MultiMap<String> headers = new MultiMap<String>();
367             while(true)
368             {
369                 bytes=readLine(_in);
370                 if(bytes==null)
371                     break outer;
372 
373                 // If blank line, end of part headers
374                 if(bytes.length==0)
375                     break;
376 
377                 total += bytes.length;
378                 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
379                     throw new IOException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
380 
381                 line=new String(bytes,"UTF-8");
382 
383                 //get content-disposition and content-type
384                 int c=line.indexOf(':',0);
385                 if(c>0)
386                 {
387                     String key=line.substring(0,c).trim().toLowerCase();
388                     String value=line.substring(c+1,line.length()).trim();
389                     headers.put(key, value);
390                     if (key.equalsIgnoreCase("content-disposition"))
391                         contentDisposition=value;
392                     if (key.equalsIgnoreCase("content-type"))
393                         contentType = value;
394                 }
395             }
396 
397             // Extract content-disposition
398             boolean form_data=false;
399             if(contentDisposition==null)
400             {
401                 throw new IOException("Missing content-disposition");
402             }
403 
404             StringTokenizer tok=new StringTokenizer(contentDisposition,";");
405             String name=null;
406             String filename=null;
407             while(tok.hasMoreTokens())
408             {
409                 String t=tok.nextToken().trim();
410                 String tl=t.toLowerCase();
411                 if(t.startsWith("form-data"))
412                     form_data=true;
413                 else if(tl.startsWith("name="))
414                     name=value(t);
415                 else if(tl.startsWith("filename="))
416                     filename=value(t);
417             }
418 
419             // Check disposition
420             if(!form_data)
421             {
422                 continue;
423             }
424             //It is valid for reset and submit buttons to have an empty name.
425             //If no name is supplied, the browser skips sending the info for that field.
426             //However, if you supply the empty string as the name, the browser sends the
427             //field, with name as the empty string. So, only continue this loop if we
428             //have not yet seen a name field.
429             if(name==null)
430             {
431                 continue;
432             }
433 
434             //Have a new Part
435             MultiPart part = new MultiPart(name, filename);
436             part.setHeaders(headers);
437             part.setContentType(contentType);
438             _parts.put(name, part);
439 
440             part.open();
441 
442             try
443             {
444                 int state=-2;
445                 int c;
446                 boolean cr=false;
447                 boolean lf=false;
448 
449                 // loop for all lines`
450                 while(true)
451                 {
452                     int b=0;
453                     while((c=(state!=-2)?state:_in.read())!=-1)
454                     {
455                         total ++;
456                         if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
457                             throw new IOException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
458 
459                         state=-2;
460                         // look for CR and/or LF
461                         if(c==13||c==10)
462                         {
463                             if(c==13)
464                                 state=_in.read();
465                             break;
466                         }
467                         // look for boundary
468                         if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
469                             b++;
470                         else
471                         {
472                             // this is not a boundary
473                             if(cr)
474                                 part.write(13);
475 
476                             if(lf)
477                                 part.write(10);
478 
479                             cr=lf=false;
480                             if(b>0)
481                                 part.write(byteBoundary,0,b);
482 
483                             b=-1;
484                             part.write(c);
485                         }
486                     }
487                     // check partial boundary
488                     if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
489                     {
490                         if(cr)
491                             part.write(13);
492 
493                         if(lf)
494                             part.write(10);
495 
496                         cr=lf=false;
497                         part.write(byteBoundary,0,b);
498                         b=-1;
499                     }
500                     // boundary match
501                     if(b>0||c==-1)
502                     {
503                         if(b==byteBoundary.length)
504                             lastPart=true;
505                         if(state==10)
506                             state=-2;
507                         break;
508                     }
509                     // handle CR LF
510                     if(cr)
511                         part.write(13);
512 
513                     if(lf)
514                         part.write(10);
515 
516                     cr=(c==13);
517                     lf=(c==10||state==10);
518                     if(state==10)
519                         state=-2;
520                 }
521             }
522             finally
523             {
524 
525                 part.close();
526             }
527         }
528     }
529 
530 
531     /* ------------------------------------------------------------ */
532     private String value(String nameEqualsValue)
533     {
534         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
535         int i=value.indexOf(';');
536         if(i>0)
537             value=value.substring(0,i);
538         if(value.startsWith("\""))
539         {
540             value=value.substring(1,value.indexOf('"',1));
541         }
542         else
543         {
544             i=value.indexOf(' ');
545             if(i>0)
546                 value=value.substring(0,i);
547         }
548         return value;
549     }
550 
551     public static int CR = '\015';
552     public static int LF = '\012';
553 
554     private byte[] readLine(InputStream in) throws IOException
555     {
556         byte[] buf = new byte[256];
557 
558         int i=0;
559         int loops=0;
560         int ch=0;
561 
562         while (true)
563         {
564             ch=in.read();
565             if (ch<0)
566                 break;
567             loops++;
568 
569             // skip a leading LF's
570             if (loops==1 && ch==LF)
571                 continue;
572 
573             if (ch==CR || ch==LF)
574                 break;
575 
576             if (i>=buf.length)
577             {
578                 byte[] old_buf=buf;
579                 buf=new byte[old_buf.length+256];
580                 System.arraycopy(old_buf, 0, buf, 0, old_buf.length);
581             }
582             buf[i++]=(byte)ch;
583         }
584 
585         if (ch==-1 && i==0)
586             return null;
587 
588         // skip a trailing LF if it exists
589         if (ch==CR && in.available()>=1 && in.markSupported())
590         {
591             in.mark(1);
592             ch=in.read();
593             if (ch!=LF)
594                 in.reset();
595         }
596 
597         byte[] old_buf=buf;
598         buf=new byte[i];
599         System.arraycopy(old_buf, 0, buf, 0, i);
600 
601         return buf;
602     }
603 
604 }