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