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.ByteArrayOutputStream;
18  import java.io.File;
19  import java.io.FileNotFoundException;
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.MultiMap;
43  import org.mortbay.util.StringUtil;
44  import org.mortbay.util.TypeUtil;
45  
46  /* ------------------------------------------------------------ */
47  /**
48   * Multipart Form Data Filter.
49   * <p>
50   * This class decodes the multipart/form-data stream sent by a HTML form that uses a file input
51   * item.  Any files sent are stored to a tempary file and a File object added to the request 
52   * as an attribute.  All other values are made available via the normal getParameter API and
53   * the setCharacterEncoding mechanism is respected when converting bytes to Strings.
54   * 
55   * If the init paramter "delete" is set to "true", any files created will be deleted when the
56   * current request returns.
57   * 
58   * @author Greg Wilkins
59   * @author Jim Crossley
60   */
61  public class MultiPartFilter implements Filter
62  {
63      private final static String FILES ="org.mortbay.servlet.MultiPartFilter.files";
64      private File tempdir;
65      private boolean _deleteFiles;
66      private ServletContext _context;
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          _context=filterConfig.getServletContext();
77      }
78  
79      /* ------------------------------------------------------------------------------- */
80      /**
81       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
82       *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
83       */
84      public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain) 
85          throws IOException, ServletException
86      {
87          HttpServletRequest srequest=(HttpServletRequest)request;
88          if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
89          {
90              chain.doFilter(request,response);
91              return;
92          }
93          
94          BufferedInputStream in = new BufferedInputStream(request.getInputStream());
95          String content_type=srequest.getContentType();
96          
97          // TODO - handle encodings
98          
99          String boundary="--"+value(content_type.substring(content_type.indexOf("boundary=")));
100         byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
101         MultiMap params = new MultiMap();
102         
103         try
104         {
105             // Get first boundary
106             byte[] bytes=TypeUtil.readLine(in);
107             String line=bytes==null?null:new String(bytes,"UTF-8");
108             if(line==null || !line.equals(boundary))
109             {
110                 throw new IOException("Missing initial multi part boundary");
111             }
112             
113             // Read each part
114             boolean lastPart=false;
115             String content_disposition=null;
116             while(!lastPart)
117             {
118                 while(true)
119                 {
120                     bytes=TypeUtil.readLine(in);
121                     // If blank line, end of part headers
122                     if(bytes==null || bytes.length==0)
123                         break;
124                     line=new String(bytes,"UTF-8");
125                     
126                     // place part header key and value in map
127                     int c=line.indexOf(':',0);
128                     if(c>0)
129                     {
130                         String key=line.substring(0,c).trim().toLowerCase();
131                         String value=line.substring(c+1,line.length()).trim();
132                         if(key.equals("content-disposition"))
133                             content_disposition=value;
134                     }
135                 }
136                 // Extract content-disposition
137                 boolean form_data=false;
138                 if(content_disposition==null)
139                 {
140                     throw new IOException("Missing content-disposition");
141                 }
142                 
143                 StringTokenizer tok=new StringTokenizer(content_disposition,";");
144                 String name=null;
145                 String filename=null;
146                 while(tok.hasMoreTokens())
147                 {
148                     String t=tok.nextToken().trim();
149                     String tl=t.toLowerCase();
150                     if(t.startsWith("form-data"))
151                         form_data=true;
152                     else if(tl.startsWith("name="))
153                         name=value(t);
154                     else if(tl.startsWith("filename="))
155                         filename=value(t);
156                 }
157                 
158                 // Check disposition
159                 if(!form_data)
160                 {
161                     continue;
162                 }
163                 if(name==null||name.length()==0)
164                 {
165                     continue;
166                 }
167                 
168                 OutputStream out=null;
169                 File file=null;
170                 try
171                 {
172                     if (filename!=null && filename.length()>0)
173                     {
174                         file = File.createTempFile("MultiPart", "", tempdir);
175                         out = new FileOutputStream(file);
176                         request.setAttribute(name,file);
177                         params.put(name, filename);
178                         
179                         if (_deleteFiles)
180                         {
181                             file.deleteOnExit();
182                             ArrayList files = (ArrayList)request.getAttribute(FILES);
183                             if (files==null)
184                             {
185                                 files=new ArrayList();
186                                 request.setAttribute(FILES,files);
187                             }
188                             files.add(file);
189                         }
190                         
191                     }
192                     else
193                         out=new ByteArrayOutputStream();
194                     
195                     int state=-2;
196                     int c;
197                     boolean cr=false;
198                     boolean lf=false;
199                     
200                     // loop for all lines`
201                     while(true)
202                     {
203                         int b=0;
204                         while((c=(state!=-2)?state:in.read())!=-1)
205                         {
206                             state=-2;
207                             // look for CR and/or LF
208                             if(c==13||c==10)
209                             {
210                                 if(c==13)
211                                     state=in.read();
212                                 break;
213                             }
214                             // look for boundary
215                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
216                                 b++;
217                             else
218                             {
219                                 // this is not a boundary
220                                 if(cr)
221                                     out.write(13);
222                                 if(lf)
223                                     out.write(10);
224                                 cr=lf=false;
225                                 if(b>0)
226                                     out.write(byteBoundary,0,b);
227                                 b=-1;
228                                 out.write(c);
229                             }
230                         }
231                         // check partial boundary
232                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
233                         {
234                             if(cr)
235                                 out.write(13);
236                             if(lf)
237                                 out.write(10);
238                             cr=lf=false;
239                             out.write(byteBoundary,0,b);
240                             b=-1;
241                         }
242                         // boundary match
243                         if(b>0||c==-1)
244                         {
245                             if(b==byteBoundary.length)
246                                 lastPart=true;
247                             if(state==10)
248                                 state=-2;
249                             break;
250                         }
251                         // handle CR LF
252                         if(cr)
253                             out.write(13);
254                         if(lf)
255                             out.write(10);
256                         cr=(c==13);
257                         lf=(c==10||state==10);
258                         if(state==10)
259                             state=-2;
260                     }
261                 }
262                 finally
263                 {
264                     out.close();
265                 }
266                 
267                 if (file==null)
268                 {
269                     bytes = ((ByteArrayOutputStream)out).toByteArray();
270                     params.add(name,bytes);
271                 }
272             }
273         
274             // handle request
275             chain.doFilter(new Wrapper(srequest,params),response);
276         }
277         finally
278         {
279             deleteFiles(request);
280         }
281     }
282 
283     private void deleteFiles(ServletRequest request)
284     {
285         ArrayList files = (ArrayList)request.getAttribute(FILES);
286         if (files!=null)
287         {
288             Iterator iter = files.iterator();
289             while (iter.hasNext())
290             {
291                 File file=(File)iter.next();
292                 try
293                 {
294                     file.delete();
295                 }
296                 catch(Exception e)
297                 {
298                     _context.log("failed to delete "+file,e);
299                 }
300             }
301         }
302     }
303     /* ------------------------------------------------------------ */
304     private String value(String nameEqualsValue)
305     {
306         String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
307         int i=value.indexOf(';');
308         if(i>0)
309             value=value.substring(0,i);
310         if(value.startsWith("\""))
311         {
312             value=value.substring(1,value.indexOf('"',1));
313         }
314         else
315         {
316             i=value.indexOf(' ');
317             if(i>0)
318                 value=value.substring(0,i);
319         }
320         return value;
321     }
322 
323     /* ------------------------------------------------------------------------------- */
324     /**
325      * @see javax.servlet.Filter#destroy()
326      */
327     public void destroy()
328     {
329     }
330     
331     private static class Wrapper extends HttpServletRequestWrapper
332     {
333         String encoding="UTF-8";
334         MultiMap map;
335         
336         /* ------------------------------------------------------------------------------- */
337         /** Constructor.
338          * @param request
339          */
340         public Wrapper(HttpServletRequest request, MultiMap map)
341         {
342             super(request);
343             this.map=map;
344         }
345         
346         /* ------------------------------------------------------------------------------- */
347         /**
348          * @see javax.servlet.ServletRequest#getContentLength()
349          */
350         public int getContentLength()
351         {
352             return 0;
353         }
354         
355         /* ------------------------------------------------------------------------------- */
356         /**
357          * @see javax.servlet.ServletRequest#getParameter(java.lang.String)
358          */
359         public String getParameter(String name)
360         {
361             Object o=map.get(name);
362             if (o instanceof byte[])
363             {
364                 try
365                 {
366                     String s=new String((byte[])o,encoding);
367                     return s;
368                 }
369                 catch(Exception e)
370                 {
371                     e.printStackTrace();
372                 }
373             }
374             else if (o instanceof String)
375                 return (String)o;
376             return null;
377         }
378         
379         /* ------------------------------------------------------------------------------- */
380         /**
381          * @see javax.servlet.ServletRequest#getParameterMap()
382          */
383         public Map getParameterMap()
384         {
385             return map;
386         }
387         
388         /* ------------------------------------------------------------------------------- */
389         /**
390          * @see javax.servlet.ServletRequest#getParameterNames()
391          */
392         public Enumeration getParameterNames()
393         {
394             return Collections.enumeration(map.keySet());
395         }
396         
397         /* ------------------------------------------------------------------------------- */
398         /**
399          * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String)
400          */
401         public String[] getParameterValues(String name)
402         {
403             List l=map.getValues(name);
404             if (l==null || l.size()==0)
405                 return new String[0];
406             String[] v = new String[l.size()];
407             for (int i=0;i<l.size();i++)
408             {
409                 Object o=l.get(i);
410                 if (o instanceof byte[])
411                 {
412                     try
413                     {
414                         v[i]=new String((byte[])o,encoding);
415                     }
416                     catch(Exception e)
417                     {
418                         e.printStackTrace();
419                     }
420                 }
421                 else if (o instanceof String)
422                     v[i]=(String)o;
423             }
424             return v;
425         }
426         
427         /* ------------------------------------------------------------------------------- */
428         /**
429          * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String)
430          */
431         public void setCharacterEncoding(String enc) 
432             throws UnsupportedEncodingException
433         {
434             encoding=enc;
435         }
436     }
437 }