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