View Javadoc

1   /*
2    * $Id:  $
3    * -------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  package org.mule.transport.http.multipart;
11  // ========================================================================
12  // Copyright (c) 2006-2010 Mort Bay Consulting Pty. Ltd.
13  // ------------------------------------------------------------------------
14  // All rights reserved. This program and the accompanying materials
15  // are made available under the terms of the Eclipse Public License v1.0
16  // and Apache License v2.0 which accompanies this distribution.
17  // The Eclipse Public License is available at
18  // http://www.eclipse.org/legal/epl-v10.html
19  // The Apache License v2.0 is available at
20  // http://www.opensource.org/licenses/apache2.0.php
21  // You may elect to redistribute this code under either of these licenses.
22  // ========================================================================
23  
24  
25  import java.io.BufferedInputStream;
26  import java.io.BufferedOutputStream;
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.FileNotFoundException;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.OutputStream;
36  import java.util.Collection;
37  import java.util.HashMap;
38  import java.util.Map;
39  import java.util.StringTokenizer;
40  
41  import javax.servlet.ServletException;
42  
43  
44  /**
45  * MultipartInputStream
46  *
47  * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
48  */
49  public class MultiPartInputStream
50  {
51      public static final MultipartConfiguration __DEFAULT_MULTIPART_CONFIG = new MultipartConfiguration(System.getProperty("java.io.tmpdir"));
52      protected InputStream _in;
53      protected MultipartConfiguration _config;
54      protected String _contentType;
55      protected MultiMap _map;
56      protected Map<String, Part> _parts;
57      protected File _tmpDir;
58  
59  
60  
61  
62      public class MultiPart implements Part
63      {
64          protected String _name;
65          protected String _filename;
66          protected File _file;
67          protected OutputStream _out;
68          protected String _contentType;
69          protected MultiMap<String> _headers;
70          protected long _size = 0;
71  
72          public MultiPart (String name, String filename)
73          throws IOException
74          {
75              _name = name;
76              _filename = filename;
77          }
78  
79          protected void setContentType (String contentType)
80          {
81              _contentType = contentType;
82          }
83  
84  
85          protected void open()
86          throws FileNotFoundException, IOException
87          {
88              //We will either be writing to a file, if it has a filename on the content-disposition
89              //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
90              //will need to change to write to a file.
91              if (_filename != null && _filename.trim().length() > 0)
92              {
93                  createFile();
94              }
95              else
96              {
97                  //Write to a buffer in memory until we discover we've exceed the
98                  //MultipartConfig fileSizeThreshold
99                  _out = new ByteArrayOutputStream();
100             }
101         }
102 
103         protected void close()
104         throws IOException
105         {
106             _out.close();
107         }
108 
109 
110         protected void write (int b)
111         throws IOException
112         {
113             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStream.this._config.getMaxFileSize())
114                 throw new IOException ("Multipart Mime part "+_name+" exceeds max filesize");
115 
116             if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
117                 createFile();
118             _out.write(b);
119             _size ++;
120         }
121 
122         protected void write (byte[] bytes, int offset, int length)
123         throws IOException
124         {
125             if (MultiPartInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStream.this._config.getMaxFileSize())
126                 throw new IOException ("Multipart Mime part "+_name+" exceeds max filesize");
127 
128             if (MultiPartInputStream.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStream.this._config.getFileSizeThreshold() && _file==null)
129                 createFile();
130 
131             _out.write(bytes, offset, length);
132             _size += length;
133         }
134 
135         protected void createFile ()
136         throws IOException
137         {
138             _file = File.createTempFile("MultiPart", "", MultiPartInputStream.this._tmpDir);
139             FileOutputStream fos = new FileOutputStream(_file);
140             BufferedOutputStream bos = new BufferedOutputStream(fos);
141 
142             if (_size > 0 && _out != null)
143             {
144                 //already written some bytes, so need to copy them into the file
145                 _out.flush();
146                 ((ByteArrayOutputStream)_out).writeTo(bos);
147                 _out.close();
148             }
149             _out = bos;
150         }
151 
152 
153 
154         protected void setHeaders(MultiMap<String> headers)
155         {
156             _headers = headers;
157         }
158 
159         /**
160          * @see Part#getContentType()
161          */
162         public String getContentType()
163         {
164             return _contentType;
165         }
166 
167         /**
168          * @see Part#getHeader(java.lang.String)
169          */
170         public String getHeader(String name)
171         {
172             return (String)_headers.getValue(name, 0);
173         }
174 
175         /**
176          * @see Part#getHeaderNames()
177          */
178         public Collection<String> getHeaderNames()
179         {
180             return _headers.keySet();
181         }
182 
183         /**
184          * @see Part#getHeaders(java.lang.String)
185          */
186         public Collection<String> getHeaders(String name)
187         {
188            return _headers.getValues(name);
189         }
190 
191         /**
192          * @see Part#getInputStream()
193          */
194         public InputStream getInputStream() throws IOException
195         {
196            if (_file != null)
197            {
198                return new BufferedInputStream (new FileInputStream(_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 }