View Javadoc

1   // ========================================================================
2   // Copyright 1996-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  package org.mortbay.servlet;
15  
16  import java.io.BufferedInputStream;
17  import java.io.BufferedOutputStream;
18  import java.io.ByteArrayOutputStream;
19  import java.io.File;
20  import java.io.FileNotFoundException;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.Enumeration;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.StringTokenizer;
32  
33  import javax.servlet.Filter;
34  import javax.servlet.FilterChain;
35  import javax.servlet.FilterConfig;
36  import javax.servlet.ServletContext;
37  import javax.servlet.ServletException;
38  import javax.servlet.ServletRequest;
39  import javax.servlet.ServletResponse;
40  import javax.servlet.http.HttpServletRequest;
41  import javax.servlet.http.HttpServletRequestWrapper;
42  
43  import org.mortbay.util.MultiMap;
44  import org.mortbay.util.StringUtil;
45  import org.mortbay.util.TypeUtil;
46  
47  /* ------------------------------------------------------------ */
48  /**
49   * Multipart Form Data Filter.
50   * <p>
51   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
52   * item.  Any files sent are stored to a tempary file and a File object added to the request 
53   * as an attribute.  All other values are made available via the normal getParameter API and
54   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
55   * 
56   * If the init paramter "delete" is set to "true", any files created will be deleted when the
57   * current request returns.
58   * 
59   * @author Greg Wilkins
60   * @author Jim Crossley
61   */
62  public class MultiPartFilter implements Filter
63  {
64      private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
65      private File tempdir;
66      private boolean _deleteFiles;
67      private ServletContext _context;
68      private int _fileOutputBuffer = 0;
69  
70      /* ------------------------------------------------------------------------------- */
71      /**
72       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
73       */
74      public void init(FilterConfig filterConfig) throws ServletException
75      {
76          tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
77          _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
78          String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
79          if(fileOutputBuffer!=null)
80              _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
81          _context=filterConfig.getServletContext();
82      }
83  
84      /* ------------------------------------------------------------------------------- */
85      /**
86       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
87       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
88       */
89      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
90          throws IOException, ServletException
91      {
92          HttpServletRequest srequest=(HttpServletRequest)request;
93          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
94          {
95              chain.doFilter(request,response);
96              return;
97          }
98          
99          BufferedInputStream in = new BufferedInputStream(request.getInputStream());
100         String content_type=srequest.getContentType();
101         
102         // TODO - handle encodings
103         
104         String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
105         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
106         // cross-container
107         MultiMap params = new MultiMap(request.getParameterMap());
108         
109         // jetty-specific but more efficient
110         /*MultiMap params = new MultiMap();
111         if(srequest instanceof org.mortbay.jetty.Request)
112         {
113             org.mortbay.jetty.Request req = ((org.mortbay.jetty.Request)srequest);
114             req.getUri().decodeQueryTo(params, req.getQueryEncoding());
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                 
178                 //It is valid for reset and submit buttons to have an empty name.
179                 //If no name is supplied, the browser skips sending the info for that field.
180                 //However, if you supply the empty string as the name, the browser sends the
181                 //field, with name as the empty string. So, only continue this loop if we
182                 //have not yet seen a name field.
183                 if(name==null)
184                 {
185                     continue;
186                 }
187                 
188                 OutputStream out=null;
189                 File file=null;
190                 try
191                 {
192                     if (filename!=null && filename.length()>0)
193                     {
194                         file = File.createTempFile("MultiPart", "", tempdir);
195                         out = new FileOutputStream(file);
196                         if(_fileOutputBuffer>0)
197                             out = new BufferedOutputStream(out, _fileOutputBuffer);
198                         request.setAttribute(name,file);
199                         params.put(name, filename);
200                         
201                         if (_deleteFiles)
202                         {
203                             file.deleteOnExit();
204                             ArrayList files = (ArrayList)request.getAttribute(FILES);
205                             if (files==null)
206                             {
207                                 files=new ArrayList();
208                                 request.setAttribute(FILES,files);
209                             }
210                             files.add(file);
211                         }
212                         
213                     }
214                     else
215                         out=new ByteArrayOutputStream();
216                     
217                     int state=-2;
218                     int c;
219                     boolean cr=false;
220                     boolean lf=false;
221                     
222                     // loop for all lines`
223                     while(true)
224                     {
225                         int b=0;
226                         while((c=(state!=-2)?state:in.read())!=-1)
227                         {
228                             state=-2;
229                             // look for CR and/or LF
230                             if(c==13||c==10)
231                             {
232                                 if(c==13)
233                                     state=in.read();
234                                 break;
235                             }
236                             // look for boundary
237                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
238                                 b++;
239                             else
240                             {
241                                 // this is not a boundary
242                                 if(cr)
243                                     out.write(13);
244                                 if(lf)
245                                     out.write(10);
246                                 cr=lf=false;
247                                 if(b>0)
248                                     out.write(byteBoundary,0,b);
249                                 b=-1;
250                                 out.write(c);
251                             }
252                         }
253                         // check partial boundary
254                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
255                         {
256                             if(cr)
257                                 out.write(13);
258                             if(lf)
259                                 out.write(10);
260                             cr=lf=false;
261                             out.write(byteBoundary,0,b);
262                             b=-1;
263                         }
264                         // boundary match
265                         if(b>0||c==-1)
266                         {
267                             if(b==byteBoundary.length)
268                                 lastPart=true;
269                             if(state==10)
270                                 state=-2;
271                             break;
272                         }
273                         // handle CR LF
274                         if(cr)
275                             out.write(13);
276                         if(lf)
277                             out.write(10);
278                         cr=(c==13);
279                         lf=(c==10||state==10);
280                         if(state==10)
281                             state=-2;
282                     }
283                 }
284                 finally
285                 {
286                     out.close();
287                 }
288                 
289                 if (file==null)
290                 {
291                     bytes = ((ByteArrayOutputStream)out).toByteArray();
292                     params.add(name,bytes);
293                 }
294             }
295         
296             // handle request
297             chain.doFilter(new Wrapper(srequest,params),response);
298         }
299         finally
300         {
301             deleteFiles(request);
302         }
303     }
304 
305     private void deleteFiles(ServletRequest request)
306     {
307         ArrayList files = (ArrayList)request.getAttribute(FILES);
308         if (files!=null)
309         {
310             Iterator iter = files.iterator();
311             while (iter.hasNext())
312             {
313                 File file=(File)iter.next();
314                 try
315                 {
316                     file.delete();
317                 }
318                 catch(Exception e)
319                 {
320                     _context.log("failed to delete "+file,e);
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     private static class Wrapper extends HttpServletRequestWrapper
354     {
355         String encoding="UTF-8";
356         MultiMap map;
357         
358         /* ------------------------------------------------------------------------------- */
359         /** Constructor.
360          * @param request
361          */
362         public Wrapper(HttpServletRequest request, MultiMap map)
363         {
364             super(request);
365             this.map=map;
366         }
367         
368         /* ------------------------------------------------------------------------------- */
369         /**
370          * @see javax.servlet.ServletRequest#getContentLength()
371          */
372         public int getContentLength()
373         {
374             return 0;
375         }
376         
377         /* ------------------------------------------------------------------------------- */
378         /**
379          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
380          */
381         public String getParameter(String name)
382         {
383             Object o=map.get(name);
384             if (o instanceof byte[])
385             {
386                 try
387                 {
388                     String s=new String((byte[])o,encoding);
389                     return s;
390                 }
391                 catch(Exception e)
392                 {
393                     e.printStackTrace();
394                 }
395             }
396             else if (o instanceof String)
397                 return (String)o;
398             else if (o instanceof String[])
399             {
400                 String[] s = (String[])o;
401                 return s.length>0 ? s[0] : null;
402             }
403             return null;
404         }
405         
406         /* ------------------------------------------------------------------------------- */
407         /**
408          * @see javax.servlet.ServletRequest#getParameterMap()
409          */
410         public Map getParameterMap()
411         {
412             return map;
413         }
414         
415         /* ------------------------------------------------------------------------------- */
416         /**
417          * @see javax.servlet.ServletRequest#getParameterNames()
418          */
419         public Enumeration getParameterNames()
420         {
421             return Collections.enumeration(map.keySet());
422         }
423         
424         /* ------------------------------------------------------------------------------- */
425         /**
426          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
427          */
428         public String[] getParameterValues(String name)
429         {
430             List l=map.getValues(name);
431             if (l==null || l.size()==0)
432                 return new String[0];
433             String[] v = new String[l.size()];
434             for (int i=0;i<l.size();i++)
435             {
436                 Object o=l.get(i);
437                 if (o instanceof byte[])
438                 {
439                     try
440                     {
441                         v[i]=new String((byte[])o,encoding);
442                     }
443                     catch(Exception e)
444                     {
445                         e.printStackTrace();
446                     }
447                 }
448                 else if (o instanceof String)
449                     v[i]=(String)o;
450             }
451             return v;
452         }
453         
454         /* ------------------------------------------------------------------------------- */
455         /**
456          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
457          */
458         public void setCharacterEncoding(String enc) 
459             throws UnsupportedEncodingException
460         {
461             encoding=enc;
462         }
463     }
464 }