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.LazyList;
44  import org.mortbay.util.MultiMap;
45  import org.mortbay.util.StringUtil;
46  import org.mortbay.util.TypeUtil;
47  
48  /* ------------------------------------------------------------ */
49  /**
50   * Multipart Form Data Filter.
51   * <p>
52   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
53   * item.  Any files sent are stored to a tempary file and a File object added to the request 
54   * as an attribute.  All other values are made available via the normal getParameter API and
55   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
56   * 
57   * If the init paramter "delete" is set to "true", any files created will be deleted when the
58   * current request returns.
59   * 
60   * @author Greg Wilkins
61   * @author Jim Crossley
62   */
63  public class MultiPartFilter implements Filter
64  {
65      private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
66      private File tempdir;
67      private boolean _deleteFiles;
68      private ServletContext _context;
69      private int _fileOutputBuffer = 0;
70  
71      /* ------------------------------------------------------------------------------- */
72      /**
73       * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
74       */
75      public void init(FilterConfig filterConfig) throws ServletException
76      {
77          tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
78          _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
79          String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
80          if(fileOutputBuffer!=null)
81              _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
82          _context=filterConfig.getServletContext();
83      }
84  
85      /* ------------------------------------------------------------------------------- */
86      /**
87       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
88       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
89       */
90      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
91          throws IOException, ServletException
92      {
93          HttpServletRequest srequest=(HttpServletRequest)request;
94          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
95          {
96              chain.doFilter(request,response);
97              return;
98          }
99          
100         BufferedInputStream in = new BufferedInputStream(request.getInputStream());
101         String content_type=srequest.getContentType();
102         
103         // TODO - handle encodings
104         
105         String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
106         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
107         
108         MultiMap params = new MultiMap();
109         for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
110         {
111             Map.Entry entry=(Map.Entry)i.next();
112             Object value=entry.getValue();
113             if (value instanceof String[])
114                 params.addValues(entry.getKey(),(String[])value);
115             else
116                 params.add(entry.getKey(),value);
117         }
118         
119         try
120         {
121             // Get first boundary
122             byte[] bytes=TypeUtil.readLine(in);
123             String line=bytes==null?null:new String(bytes,"UTF-8");
124             if(line==null || !line.equals(boundary))
125             {
126                 throw new IOException("Missing initial multi part boundary");
127             }
128             
129             // Read each part
130             boolean lastPart=false;
131             String content_disposition=null;
132             while(!lastPart)
133             {
134                 while(true)
135                 {
136                     bytes=TypeUtil.readLine(in);
137                     // If blank line, end of part headers
138                     if(bytes==null || bytes.length==0)
139                         break;
140                     line=new String(bytes,"UTF-8");
141                     
142                     // place part header key and value in map
143                     int c=line.indexOf(':',0);
144                     if(c>0)
145                     {
146                         String key=line.substring(0,c).trim().toLowerCase();
147                         String value=line.substring(c+1,line.length()).trim();
148                         if(key.equals("content-disposition"))
149                             content_disposition=value;
150                     }
151                 }
152                 // Extract content-disposition
153                 boolean form_data=false;
154                 if(content_disposition==null)
155                 {
156                     throw new IOException("Missing content-disposition");
157                 }
158                 
159                 StringTokenizer tok=new StringTokenizer(content_disposition,";");
160                 String name=null;
161                 String filename=null;
162                 while(tok.hasMoreTokens())
163                 {
164                     String t=tok.nextToken().trim();
165                     String tl=t.toLowerCase();
166                     if(t.startsWith("form-data"))
167                         form_data=true;
168                     else if(tl.startsWith("name="))
169                         name=value(t);
170                     else if(tl.startsWith("filename="))
171                         filename=value(t);
172                 }
173                 
174                 // Check disposition
175                 if(!form_data)
176                 {
177                     continue;
178                 }
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     private String value(String nameEqualsValue)
329     {
330         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
331         int i=value.indexOf(';');
332         if(i>0)
333             value=value.substring(0,i);
334         if(value.startsWith("\""))
335         {
336             value=value.substring(1,value.indexOf('"',1));
337         }
338         else
339         {
340             i=value.indexOf(' ');
341             if(i>0)
342                 value=value.substring(0,i);
343         }
344         return value;
345     }
346 
347     /* ------------------------------------------------------------------------------- */
348     /**
349      * @see javax.servlet.Filter#destroy()
350      */
351     public void destroy()
352     {
353     }
354     
355     private static class Wrapper extends HttpServletRequestWrapper
356     {
357         String encoding="UTF-8";
358         MultiMap map;
359         
360         /* ------------------------------------------------------------------------------- */
361         /** Constructor.
362          * @param request
363          */
364         public Wrapper(HttpServletRequest request, MultiMap map)
365         {
366             super(request);
367             this.map=map;
368         }
369         
370         /* ------------------------------------------------------------------------------- */
371         /**
372          * @see javax.servlet.ServletRequest#getContentLength()
373          */
374         public int getContentLength()
375         {
376             return 0;
377         }
378         
379         /* ------------------------------------------------------------------------------- */
380         /**
381          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
382          */
383         public String getParameter(String name)
384         {
385             Object o=map.get(name);
386             if (!(o instanceof byte[]) && LazyList.size(o)>0)
387                 o=LazyList.get(o,0);
388             
389             if (o instanceof byte[])
390             {
391                 try
392                 {
393                     String s=new String((byte[])o,encoding);
394                     return s;
395                 }
396                 catch(Exception e)
397                 {
398                     e.printStackTrace();
399                 }
400             }
401             else if (o!=null)
402                 return String.valueOf(o);
403             return null;
404         }
405         
406         /* ------------------------------------------------------------------------------- */
407         /**
408          * @see javax.servlet.ServletRequest#getParameterMap()
409          */
410         public Map getParameterMap()
411         {
412             return Collections.unmodifiableMap(map.toStringArrayMap());
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 }