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