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                 //It is valid for reset and submit buttons to have an empty name.
176                 //If no name is supplied, the browser skips sending the info for that field.
177                 //However, if you supply the empty string as the name, the browser sends the
178                 //field, with name as the empty string. So, only continue this loop if we
179                 //have not yet seen a name field.
180                 if(name==null)
181                 {
182                     continue;
183                 }
184                 
185                 OutputStream out=null;
186                 File file=null;
187                 try
188                 {
189                     if (filename!=null && filename.length()>0)
190                     {
191                         file = File.createTempFile("MultiPart", "", tempdir);
192                         out = new FileOutputStream(file);
193                         if(_fileOutputBuffer>0)
194                             out = new BufferedOutputStream(out, _fileOutputBuffer);
195                         request.setAttribute(name,file);
196                         params.put(name, filename);
197                         
198                         if (_deleteFiles)
199                         {
200                             file.deleteOnExit();
201                             ArrayList files = (ArrayList)request.getAttribute(FILES);
202                             if (files==null)
203                             {
204                                 files=new ArrayList();
205                                 request.setAttribute(FILES,files);
206                             }
207                             files.add(file);
208                         }
209                         
210                     }
211                     else
212                         out=new ByteArrayOutputStream();
213                     
214                     int state=-2;
215                     int c;
216                     boolean cr=false;
217                     boolean lf=false;
218                     
219                     // loop for all lines`
220                     while(true)
221                     {
222                         int b=0;
223                         while((c=(state!=-2)?state:in.read())!=-1)
224                         {
225                             state=-2;
226                             // look for CR and/or LF
227                             if(c==13||c==10)
228                             {
229                                 if(c==13)
230                                     state=in.read();
231                                 break;
232                             }
233                             // look for boundary
234                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
235                                 b++;
236                             else
237                             {
238                                 // this is not a boundary
239                                 if(cr)
240                                     out.write(13);
241                                 if(lf)
242                                     out.write(10);
243                                 cr=lf=false;
244                                 if(b>0)
245                                     out.write(byteBoundary,0,b);
246                                 b=-1;
247                                 out.write(c);
248                             }
249                         }
250                         // check partial boundary
251                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
252                         {
253                             if(cr)
254                                 out.write(13);
255                             if(lf)
256                                 out.write(10);
257                             cr=lf=false;
258                             out.write(byteBoundary,0,b);
259                             b=-1;
260                         }
261                         // boundary match
262                         if(b>0||c==-1)
263                         {
264                             if(b==byteBoundary.length)
265                                 lastPart=true;
266                             if(state==10)
267                                 state=-2;
268                             break;
269                         }
270                         // handle CR LF
271                         if(cr)
272                             out.write(13);
273                         if(lf)
274                             out.write(10);
275                         cr=(c==13);
276                         lf=(c==10||state==10);
277                         if(state==10)
278                             state=-2;
279                     }
280                 }
281                 finally
282                 {
283                     out.close();
284                 }
285                 
286                 if (file==null)
287                 {
288                     bytes = ((ByteArrayOutputStream)out).toByteArray();
289                     params.add(name,bytes);
290                 }
291             }
292         
293             // handle request
294             chain.doFilter(new Wrapper(srequest,params),response);
295         }
296         finally
297         {
298             deleteFiles(request);
299         }
300     }
301 
302     private void deleteFiles(ServletRequest request)
303     {
304         ArrayList files = (ArrayList)request.getAttribute(FILES);
305         if (files!=null)
306         {
307             Iterator iter = files.iterator();
308             while (iter.hasNext())
309             {
310                 File file=(File)iter.next();
311                 try
312                 {
313                     file.delete();
314                 }
315                 catch(Exception e)
316                 {
317                     _context.log("failed to delete "+file,e);
318                 }
319             }
320         }
321     }
322     /* ------------------------------------------------------------ */
323     private String value(String nameEqualsValue)
324     {
325         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
326         int i=value.indexOf(';');
327         if(i>0)
328             value=value.substring(0,i);
329         if(value.startsWith("\""))
330         {
331             value=value.substring(1,value.indexOf('"',1));
332         }
333         else
334         {
335             i=value.indexOf(' ');
336             if(i>0)
337                 value=value.substring(0,i);
338         }
339         return value;
340     }
341 
342     /* ------------------------------------------------------------------------------- */
343     /**
344      * @see javax.servlet.Filter#destroy()
345      */
346     public void destroy()
347     {
348     }
349     
350     private static class Wrapper extends HttpServletRequestWrapper
351     {
352         String encoding="UTF-8";
353         MultiMap map;
354         
355         /* ------------------------------------------------------------------------------- */
356         /** Constructor.
357          * @param request
358          */
359         public Wrapper(HttpServletRequest request, MultiMap map)
360         {
361             super(request);
362             this.map=map;
363         }
364         
365         /* ------------------------------------------------------------------------------- */
366         /**
367          * @see javax.servlet.ServletRequest#getContentLength()
368          */
369         public int getContentLength()
370         {
371             return 0;
372         }
373         
374         /* ------------------------------------------------------------------------------- */
375         /**
376          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
377          */
378         public String getParameter(String name)
379         {
380             Object o=map.get(name);
381             if (o instanceof byte[])
382             {
383                 try
384                 {
385                     String s=new String((byte[])o,encoding);
386                     return s;
387                 }
388                 catch(Exception e)
389                 {
390                     e.printStackTrace();
391                 }
392             }
393             else if (o instanceof String)
394                 return (String)o;
395             else if (o instanceof String[])
396             {
397                 String[] s = (String[])o;
398                 return s.length>0 ? s[0] : null;
399             }
400             return null;
401         }
402         
403         /* ------------------------------------------------------------------------------- */
404         /**
405          * @see javax.servlet.ServletRequest#getParameterMap()
406          */
407         public Map getParameterMap()
408         {
409             return map;
410         }
411         
412         /* ------------------------------------------------------------------------------- */
413         /**
414          * @see javax.servlet.ServletRequest#getParameterNames()
415          */
416         public Enumeration getParameterNames()
417         {
418             return Collections.enumeration(map.keySet());
419         }
420         
421         /* ------------------------------------------------------------------------------- */
422         /**
423          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
424          */
425         public String[] getParameterValues(String name)
426         {
427             List l=map.getValues(name);
428             if (l==null || l.size()==0)
429                 return new String[0];
430             String[] v = new String[l.size()];
431             for (int i=0;i<l.size();i++)
432             {
433                 Object o=l.get(i);
434                 if (o instanceof byte[])
435                 {
436                     try
437                     {
438                         v[i]=new String((byte[])o,encoding);
439                     }
440                     catch(Exception e)
441                     {
442                         e.printStackTrace();
443                     }
444                 }
445                 else if (o instanceof String)
446                     v[i]=(String)o;
447             }
448             return v;
449         }
450         
451         /* ------------------------------------------------------------------------------- */
452         /**
453          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
454          */
455         public void setCharacterEncoding(String enc) 
456             throws UnsupportedEncodingException
457         {
458             encoding=enc;
459         }
460     }
461 }