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