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