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