View Javadoc

1   // ========================================================================
2   // Copyright (c) 1996-2009 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  package org.eclipse.jetty.servlets;
14  
15  import java.io.BufferedInputStream;
16  import java.io.BufferedOutputStream;
17  import java.io.BufferedReader;
18  import java.io.ByteArrayOutputStream;
19  import java.io.File;
20  import java.io.FileOutputStream;
21  import java.io.FilterInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.OutputStream;
26  import java.io.UnsupportedEncodingException;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Enumeration;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  
34  import javax.servlet.Filter;
35  import javax.servlet.FilterChain;
36  import javax.servlet.FilterConfig;
37  import javax.servlet.ServletContext;
38  import javax.servlet.ServletException;
39  import javax.servlet.ServletRequest;
40  import javax.servlet.ServletResponse;
41  import javax.servlet.http.HttpServletRequest;
42  import javax.servlet.http.HttpServletRequestWrapper;
43  
44  import org.eclipse.jetty.util.B64Code;
45  import org.eclipse.jetty.util.LazyList;
46  import org.eclipse.jetty.util.MultiMap;
47  import org.eclipse.jetty.util.QuotedStringTokenizer;
48  import org.eclipse.jetty.util.StringUtil;
49  import org.eclipse.jetty.util.TypeUtil;
50  
51  /* ------------------------------------------------------------ */
52  /**
53   * Multipart Form Data Filter.
54   * <p>
55   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
56   * item.  Any files sent are stored to a temporary file and a File object added to the request 
57   * as an attribute.  All other values are made available via the normal getParameter API and
58   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
59   * <p>
60   * If the init parameter "delete" is set to "true", any files created will be deleted when the
61   * current request returns.
62   * 
63   */
64  public class MultiPartFilter implements Filter
65  {
66      private final static String FILES ="org.eclipse.jetty.servlet.MultiPartFilter.files";
67      private File tempdir;
68      private boolean _deleteFiles;
69      private ServletContext _context;
70      private int _fileOutputBuffer = 0;
71  
72      /* ------------------------------------------------------------------------------- */
73      /**
74       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
75       */
76      public void init(FilterConfig filterConfig) throws ServletException
77      {
78          tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
79          _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
80          String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
81          if(fileOutputBuffer!=null)
82              _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
83          _context=filterConfig.getServletContext();
84      }
85  
86      /* ------------------------------------------------------------------------------- */
87      /**
88       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
89       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
90       */
91      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
92          throws IOException, ServletException
93      {
94          HttpServletRequest srequest=(HttpServletRequest)request;
95          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
96          {
97              chain.doFilter(request,response);
98              return;
99          }
100         
101         InputStream in = new BufferedInputStream(request.getInputStream());
102         String content_type=srequest.getContentType();
103         
104         // TODO - handle encodings
105         String boundary="--"+QuotedStringTokenizer.unquote(value(content_type.substring(content_type.indexOf("boundary="))).trim());
106         
107         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
108         
109         MultiMap params = new MultiMap();
110         for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
111         {
112             Map.Entry entry=(Map.Entry)i.next();
113             Object value=entry.getValue();
114             if (value instanceof String[])
115                 params.addValues(entry.getKey(),(String[])value);
116             else
117                 params.add(entry.getKey(),value);
118         }
119         
120         try
121         {
122             // Get first boundary
123             byte[] bytes=TypeUtil.readLine(in);
124             String line=bytes==null?null:new String(bytes,"UTF-8");
125             if(line==null || !line.equals(boundary))
126             {
127                 throw new IOException("Missing initial multi part boundary");
128             }
129             
130             // Read each part
131             boolean lastPart=false;
132             String content_disposition=null;
133             String content_transfer_encoding=null;
134             
135             outer:while(!lastPart)
136             {
137                 while(true)
138                 {
139                     // read a line
140                     bytes=TypeUtil.readLine(in);
141                     if (bytes==null)
142                         break outer;
143                     
144                     // If blank line, end of part headers
145                     if(bytes.length==0)
146                         break;
147                     line=new String(bytes,"UTF-8");
148                     
149                     // place part header key and value in map
150                     int c=line.indexOf(':',0);
151                     if(c>0)
152                     {
153                         String key=line.substring(0,c).trim().toLowerCase();
154                         String value=line.substring(c+1,line.length()).trim();
155                         if(key.equals("content-disposition"))
156                             content_disposition=value;
157                         else if(key.equals("content-transfer-encoding"))
158                         	content_transfer_encoding=value;
159                     }
160                 }
161                 // Extract content-disposition
162                 boolean form_data=false;
163                 if(content_disposition==null)
164                 {
165                     throw new IOException("Missing content-disposition");
166                 }
167                 
168                 QuotedStringTokenizer tok=new QuotedStringTokenizer(content_disposition,";");
169                 String name=null;
170                 String filename=null;
171                 while(tok.hasMoreTokens())
172                 {
173                     String t=tok.nextToken().trim();
174                     String tl=t.toLowerCase();
175                     if(t.startsWith("form-data"))
176                         form_data=true;
177                     else if(tl.startsWith("name="))
178                         name=value(t);
179                     else if(tl.startsWith("filename="))
180                         filename=value(t);
181                 }
182                 
183                 // Check disposition
184                 if(!form_data)
185                 {
186                     continue;
187                 }
188                 //It is valid for reset and submit buttons to have an empty name.
189                 //If no name is supplied, the browser skips sending the info for that field.
190                 //However, if you supply the empty string as the name, the browser sends the
191                 //field, with name as the empty string. So, only continue this loop if we
192                 //have not yet seen a name field.
193                 if(name==null)
194                 {
195                     continue;
196                 }
197                 
198                 OutputStream out=null;
199                 File file=null;
200                 try
201                 {
202                     if (filename!=null && filename.length()>0)
203                     {
204                         file = File.createTempFile("MultiPart", "", tempdir);
205                         out = new FileOutputStream(file);
206                         if(_fileOutputBuffer>0)
207                             out = new BufferedOutputStream(out, _fileOutputBuffer);
208                         request.setAttribute(name,file);
209                         params.add(name, filename);
210                         
211                         if (_deleteFiles)
212                         {
213                             file.deleteOnExit();
214                             ArrayList files = (ArrayList)request.getAttribute(FILES);
215                             if (files==null)
216                             {
217                                 files=new ArrayList();
218                                 request.setAttribute(FILES,files);
219                             }
220                             files.add(file);
221                         }   
222                     }
223                     else
224                     {
225                         out=new ByteArrayOutputStream();
226                     }
227                     
228                     
229                     if ("base64".equalsIgnoreCase(content_transfer_encoding))
230                     {
231                         in = new Base64InputStream(in);   
232                     }
233                     else if ("quoted-printable".equalsIgnoreCase(content_transfer_encoding))
234                     {
235                         in = new FilterInputStream(in)
236                         {
237                             @Override
238                             public int read() throws IOException
239                             {
240                                 int c = in.read();
241                                 if (c >= 0 && c == '=')
242                                 {
243                                     int hi = in.read();
244                                     int lo = in.read();
245                                     if (hi < 0 || lo < 0)
246                                     {
247                                         throw new IOException("Unexpected end to quoted-printable byte");
248                                     }
249                                     char[] chars = new char[] { (char)hi, (char)lo };
250                                     c = Integer.parseInt(new String(chars),16);
251                                 }
252                                 return c;
253                             }
254                         };
255                     }
256 
257                     int state=-2;
258                     int c;
259                     boolean cr=false;
260                     boolean lf=false;
261                     
262                     // loop for all lines`
263                     while(true)
264                     {
265                         int b=0;
266                         while((c=(state!=-2)?state:in.read())!=-1)
267                         {
268                             state=-2;
269                             // look for CR and/or LF
270                             if(c==13||c==10)
271                             {
272                                 if(c==13)
273                                     state=in.read();
274                                 break;
275                             }
276                             // look for boundary
277                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
278                                 b++;
279                             else
280                             {
281                                 // this is not a boundary
282                                 if(cr)
283                                     out.write(13);
284                                 if(lf)
285                                     out.write(10);
286                                 cr=lf=false;
287                                 if(b>0)
288                                     out.write(byteBoundary,0,b);
289                                 b=-1;
290                                 out.write(c);
291                             }
292                         }
293                         // check partial boundary
294                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
295                         {
296                             if(cr)
297                                 out.write(13);
298                             if(lf)
299                                 out.write(10);
300                             cr=lf=false;
301                             out.write(byteBoundary,0,b);
302                             b=-1;
303                         }
304                         // boundary match
305                         if(b>0||c==-1)
306                         {
307                             if(b==byteBoundary.length)
308                                 lastPart=true;
309                             if(state==10)
310                                 state=-2;
311                             break;
312                         }
313                         // handle CR LF
314                         if(cr)
315                             out.write(13);
316                         if(lf)
317                             out.write(10);
318                         cr=(c==13);
319                         lf=(c==10||state==10);
320                         if(state==10)
321                             state=-2;
322                     }
323                 }
324                 finally
325                 {
326                     out.close();
327                 }
328                 
329                 if (file==null)
330                 {
331                     bytes = ((ByteArrayOutputStream)out).toByteArray();
332                     params.add(name,bytes);
333                 }
334             }
335         
336             // handle request
337             chain.doFilter(new Wrapper(srequest,params),response);
338         }
339         finally
340         {
341             deleteFiles(request);
342         }
343     }
344 
345     private void deleteFiles(ServletRequest request)
346     {
347         ArrayList files = (ArrayList)request.getAttribute(FILES);
348         if (files!=null)
349         {
350             Iterator iter = files.iterator();
351             while (iter.hasNext())
352             {
353                 File file=(File)iter.next();
354                 try
355                 {
356                     file.delete();
357                 }
358                 catch(Exception e)
359                 {
360                     _context.log("failed to delete "+file,e);
361                 }
362             }
363         }
364     }
365     
366     /* ------------------------------------------------------------ */
367     private String value(String nameEqualsValue)
368     {
369         return nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
370     }
371 
372     /* ------------------------------------------------------------------------------- */
373     /**
374      * @see javax.servlet.Filter#destroy()
375      */
376     public void destroy()
377     {
378     }
379 
380     /* ------------------------------------------------------------------------------- */
381     /* ------------------------------------------------------------------------------- */
382     private static class Wrapper extends HttpServletRequestWrapper
383     {
384         String _encoding=StringUtil.__UTF8;
385         MultiMap _params;
386         
387         /* ------------------------------------------------------------------------------- */
388         /** Constructor.
389          * @param request
390          */
391         public Wrapper(HttpServletRequest request, MultiMap map)
392         {
393             super(request);
394             this._params=map;
395         }
396         
397         /* ------------------------------------------------------------------------------- */
398         /**
399          * @see javax.servlet.ServletRequest#getContentLength()
400          */
401         @Override
402         public int getContentLength()
403         {
404             return 0;
405         }
406         
407         /* ------------------------------------------------------------------------------- */
408         /**
409          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
410          */
411         @Override
412         public String getParameter(String name)
413         {
414             Object o=_params.get(name);
415             if (!(o instanceof byte[]) && LazyList.size(o)>0)
416                 o=LazyList.get(o,0);
417             
418             if (o instanceof byte[])
419             {
420                 try
421                 {
422                     String s=new String((byte[])o,_encoding);
423                     return s;
424                 }
425                 catch(Exception e)
426                 {
427                     e.printStackTrace();
428                 }
429             }
430             else if (o!=null)
431                 return String.valueOf(o);
432             return null;
433         }
434         
435         /* ------------------------------------------------------------------------------- */
436         /**
437          * @see javax.servlet.ServletRequest#getParameterMap()
438          */
439         @Override
440         public Map getParameterMap()
441         {
442             return Collections.unmodifiableMap(_params.toStringArrayMap());
443         }
444         
445         /* ------------------------------------------------------------------------------- */
446         /**
447          * @see javax.servlet.ServletRequest#getParameterNames()
448          */
449         @Override
450         public Enumeration getParameterNames()
451         {
452             return Collections.enumeration(_params.keySet());
453         }
454         
455         /* ------------------------------------------------------------------------------- */
456         /**
457          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
458          */
459         @Override
460         public String[] getParameterValues(String name)
461         {
462             List l=_params.getValues(name);
463             if (l==null || l.size()==0)
464                 return new String[0];
465             String[] v = new String[l.size()];
466             for (int i=0;i<l.size();i++)
467             {
468                 Object o=l.get(i);
469                 if (o instanceof byte[])
470                 {
471                     try
472                     {
473                         v[i]=new String((byte[])o,_encoding);
474                     }
475                     catch(Exception e)
476                     {
477                         e.printStackTrace();
478                     }
479                 }
480                 else if (o instanceof String)
481                     v[i]=(String)o;
482             }
483             return v;
484         }
485         
486         /* ------------------------------------------------------------------------------- */
487         /**
488          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
489          */
490         @Override
491         public void setCharacterEncoding(String enc) 
492             throws UnsupportedEncodingException
493         {
494             _encoding=enc;
495         }
496     }
497     
498     private static class Base64InputStream extends InputStream
499     {
500         BufferedReader _in;
501         String _line;
502         byte[] _buffer;
503         int _pos;
504         
505         public Base64InputStream (InputStream in)
506         {
507             _in = new BufferedReader(new InputStreamReader(in));
508         }
509 
510         @Override
511         public int read() throws IOException
512         {
513             if (_buffer==null || _pos>= _buffer.length)
514             {
515                 _line = _in.readLine();
516                 if (_line==null)
517                     return -1;
518                 if (_line.startsWith("--"))
519                     _buffer=(_line+"\r\n").getBytes();
520                 else if (_line.length()==0)
521                     _buffer="\r\n".getBytes();
522                 else
523                     _buffer=B64Code.decode(_line);
524                 
525                 _pos=0;
526             }
527             return _buffer[_pos++];
528         } 
529     }
530 }