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