1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.servlets;
20
21 import java.io.BufferedOutputStream;
22 import java.io.BufferedReader;
23 import java.io.ByteArrayOutputStream;
24 import java.io.File;
25 import java.io.FileOutputStream;
26 import java.io.FilterInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.io.OutputStream;
31 import java.io.UnsupportedEncodingException;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.Enumeration;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40
41 import javax.servlet.Filter;
42 import javax.servlet.FilterChain;
43 import javax.servlet.FilterConfig;
44 import javax.servlet.ServletContext;
45 import javax.servlet.ServletException;
46 import javax.servlet.ServletRequest;
47 import javax.servlet.ServletResponse;
48 import javax.servlet.http.HttpServletRequest;
49 import javax.servlet.http.HttpServletRequestWrapper;
50
51 import org.eclipse.jetty.http.MimeTypes;
52 import org.eclipse.jetty.io.ByteArrayBuffer;
53 import org.eclipse.jetty.util.B64Code;
54 import org.eclipse.jetty.util.LazyList;
55 import org.eclipse.jetty.util.MultiMap;
56 import org.eclipse.jetty.util.QuotedStringTokenizer;
57 import org.eclipse.jetty.util.ReadLineInputStream;
58 import org.eclipse.jetty.util.StringUtil;
59 import org.eclipse.jetty.util.TypeUtil;
60 import org.eclipse.jetty.util.log.Log;
61 import org.eclipse.jetty.util.log.Logger;
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 public class MultiPartFilter implements Filter
84 {
85 private static final Logger LOG = Log.getLogger(MultiPartFilter.class);
86 public final static String CONTENT_TYPE_SUFFIX=".org.eclipse.jetty.servlet.contentType";
87 private final static String FILES ="org.eclipse.jetty.servlet.MultiPartFilter.files";
88 private File tempdir;
89 private boolean _deleteFiles;
90 private ServletContext _context;
91 private int _fileOutputBuffer = 0;
92 private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",1000).intValue();
93
94
95
96
97
98 public void init(FilterConfig filterConfig) throws ServletException
99 {
100 tempdir=(File)filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir");
101 _deleteFiles="true".equals(filterConfig.getInitParameter("deleteFiles"));
102 String fileOutputBuffer = filterConfig.getInitParameter("fileOutputBuffer");
103 if(fileOutputBuffer!=null)
104 _fileOutputBuffer = Integer.parseInt(fileOutputBuffer);
105 _context=filterConfig.getServletContext();
106 String mfks = filterConfig.getInitParameter("maxFormKeys");
107 if (mfks!=null)
108 _maxFormKeys=Integer.parseInt(mfks);
109 }
110
111
112
113
114
115
116 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
117 throws IOException, ServletException
118 {
119 HttpServletRequest srequest=(HttpServletRequest)request;
120 if(srequest.getContentType()==null||!srequest.getContentType().startsWith("multipart/form-data"))
121 {
122 chain.doFilter(request,response);
123 return;
124 }
125
126 InputStream in = new ReadLineInputStream(request.getInputStream());
127 String content_type=srequest.getContentType();
128
129
130 String contentTypeBoundary = "";
131 int bstart = content_type.indexOf("boundary=");
132 if (bstart >= 0)
133 {
134 int bend = content_type.indexOf(";", bstart);
135 bend = (bend < 0? content_type.length(): bend);
136 contentTypeBoundary = QuotedStringTokenizer.unquote(value(content_type.substring(bstart,bend)).trim());
137 }
138
139 String boundary="--"+contentTypeBoundary;
140
141 byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
142
143 MultiMap params = new MultiMap();
144 for (Iterator i = request.getParameterMap().entrySet().iterator();i.hasNext();)
145 {
146 Map.Entry entry=(Map.Entry)i.next();
147 Object value=entry.getValue();
148 if (value instanceof String[])
149 params.addValues(entry.getKey(),(String[])value);
150 else
151 params.add(entry.getKey(),value);
152 }
153
154 try
155 {
156
157 String line=((ReadLineInputStream)in).readLine();
158
159 if (line == null)
160 throw new IOException("Missing content for multipart request");
161
162 line = line.trim();
163 boolean badFormatLogged = false;
164 while (line != null && !line.equals(boundary))
165 {
166 if (!badFormatLogged)
167 {
168 LOG.warn("Badly formatted multipart request");
169 badFormatLogged = true;
170 }
171 line=((ReadLineInputStream)in).readLine();
172 line=(line==null?line:line.trim());
173 }
174
175 if (line == null)
176 throw new IOException("Missing initial multi part boundary");
177
178
179 boolean lastPart=false;
180
181 outer:while(!lastPart && params.size()<_maxFormKeys)
182 {
183 String type_content=null;
184 String content_disposition=null;
185 String content_transfer_encoding=null;
186
187 while(true)
188 {
189
190 line=((ReadLineInputStream)in).readLine();
191
192
193 if (line==null)
194 break outer;
195
196
197 if("".equals(line))
198 break;
199
200
201 int c=line.indexOf(':',0);
202 if(c>0)
203 {
204 String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
205 String value=line.substring(c+1,line.length()).trim();
206 if(key.equals("content-disposition"))
207 content_disposition=value;
208 else if(key.equals("content-transfer-encoding"))
209 content_transfer_encoding=value;
210 else if (key.equals("content-type"))
211 type_content = value;
212 }
213 }
214
215 boolean form_data=false;
216 if(content_disposition==null)
217 {
218 throw new IOException("Missing content-disposition");
219 }
220
221 LOG.debug("Content-Disposition: {}", content_disposition);
222 QuotedStringTokenizer tok=new QuotedStringTokenizer(content_disposition,";",false,true);
223 String name=null;
224 String filename=null;
225 while(tok.hasMoreTokens())
226 {
227 String t=tok.nextToken().trim();
228 String tl=t.toLowerCase();
229 if(t.startsWith("form-data"))
230 form_data=true;
231 else if(tl.startsWith("name="))
232 name=value(t);
233 else if(tl.startsWith("filename="))
234 filename=filenameValue(t);
235 }
236
237
238 if(!form_data)
239 {
240 continue;
241 }
242
243
244
245
246
247 if(name==null)
248 {
249 continue;
250 }
251
252 OutputStream out=null;
253 File file=null;
254 try
255 {
256 if (filename!=null && filename.length()>0)
257 {
258 LOG.debug("filename = \"{}\"", filename);
259 file = File.createTempFile("MultiPart", "", tempdir);
260 out = new FileOutputStream(file);
261 if(_fileOutputBuffer>0)
262 out = new BufferedOutputStream(out, _fileOutputBuffer);
263 request.setAttribute(name,file);
264 params.add(name, filename);
265 if (type_content != null)
266 params.add(name+CONTENT_TYPE_SUFFIX, type_content);
267
268 if (_deleteFiles)
269 {
270 file.deleteOnExit();
271 ArrayList files = (ArrayList)request.getAttribute(FILES);
272 if (files==null)
273 {
274 files=new ArrayList();
275 request.setAttribute(FILES,files);
276 }
277 files.add(file);
278 }
279 }
280 else
281 {
282 out=new ByteArrayOutputStream();
283 }
284
285
286 if ("base64".equalsIgnoreCase(content_transfer_encoding))
287 {
288 in = new Base64InputStream((ReadLineInputStream)in);
289 }
290 else if ("quoted-printable".equalsIgnoreCase(content_transfer_encoding))
291 {
292 in = new FilterInputStream(in)
293 {
294 @Override
295 public int read() throws IOException
296 {
297 int c = in.read();
298 if (c >= 0 && c == '=')
299 {
300 int hi = in.read();
301 int lo = in.read();
302 if (hi < 0 || lo < 0)
303 {
304 throw new IOException("Unexpected end to quoted-printable byte");
305 }
306 char[] chars = new char[] { (char)hi, (char)lo };
307 c = Integer.parseInt(new String(chars),16);
308 }
309 return c;
310 }
311 };
312 }
313
314 int state=-2;
315 int c;
316 boolean cr=false;
317 boolean lf=false;
318
319
320 while(true)
321 {
322 int b=0;
323 while((c=(state!=-2)?state:in.read())!=-1)
324 {
325 state=-2;
326
327 if(c==13||c==10)
328 {
329 if(c==13)
330 {
331 in.mark(1);
332 int tmp=in.read();
333 if (tmp!=10)
334 in.reset();
335 else
336 state=tmp;
337 }
338 break;
339 }
340
341 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
342 b++;
343 else
344 {
345
346 if(cr)
347 out.write(13);
348 if(lf)
349 out.write(10);
350 cr=lf=false;
351 if(b>0)
352 out.write(byteBoundary,0,b);
353 b=-1;
354 out.write(c);
355 }
356 }
357
358 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
359 {
360 if(cr)
361 out.write(13);
362 if(lf)
363 out.write(10);
364 cr=lf=false;
365 out.write(byteBoundary,0,b);
366 b=-1;
367 }
368
369 if(b>0||c==-1)
370 {
371 if(b==byteBoundary.length)
372 lastPart=true;
373 if(state==10)
374 state=-2;
375 break;
376 }
377
378 if(cr)
379 out.write(13);
380 if(lf)
381 out.write(10);
382 cr=(c==13);
383 lf=(c==10||state==10);
384 if(state==10)
385 state=-2;
386 }
387 }
388 finally
389 {
390 out.close();
391 }
392
393 if (file==null)
394 {
395 byte[] bytes = ((ByteArrayOutputStream)out).toByteArray();
396 params.add(name,bytes);
397 if (type_content != null)
398 params.add(name+CONTENT_TYPE_SUFFIX, type_content);
399 }
400 }
401
402
403 chain.doFilter(new Wrapper(srequest,params),response);
404 }
405 finally
406 {
407 deleteFiles(request);
408 }
409 }
410
411 private void deleteFiles(ServletRequest request)
412 {
413 ArrayList files = (ArrayList)request.getAttribute(FILES);
414 if (files!=null)
415 {
416 Iterator iter = files.iterator();
417 while (iter.hasNext())
418 {
419 File file=(File)iter.next();
420 try
421 {
422 file.delete();
423 }
424 catch(Exception e)
425 {
426 _context.log("failed to delete "+file,e);
427 }
428 }
429 }
430 }
431
432
433 private String value(String nameEqualsValue)
434 {
435 int idx = nameEqualsValue.indexOf('=');
436 String value = nameEqualsValue.substring(idx+1).trim();
437 return QuotedStringTokenizer.unquoteOnly(value);
438 }
439
440
441
442 private String filenameValue(String nameEqualsValue)
443 {
444 int idx = nameEqualsValue.indexOf('=');
445 String value = nameEqualsValue.substring(idx+1).trim();
446
447 if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
448 {
449
450
451 char first=value.charAt(0);
452 if (first=='"' || first=='\'')
453 value=value.substring(1);
454 char last=value.charAt(value.length()-1);
455 if (last=='"' || last=='\'')
456 value = value.substring(0,value.length()-1);
457
458 return value;
459 }
460 else
461
462
463
464
465 return QuotedStringTokenizer.unquoteOnly(value, true);
466 }
467
468
469
470
471
472 public void destroy()
473 {
474 }
475
476
477
478 private static class Wrapper extends HttpServletRequestWrapper
479 {
480 String _encoding=StringUtil.__UTF8;
481 MultiMap _params;
482
483
484
485
486
487 public Wrapper(HttpServletRequest request, MultiMap map)
488 {
489 super(request);
490 this._params=map;
491 }
492
493
494
495
496
497 @Override
498 public int getContentLength()
499 {
500 return 0;
501 }
502
503
504
505
506
507 @Override
508 public String getParameter(String name)
509 {
510 Object o=_params.get(name);
511 if (!(o instanceof byte[]) && LazyList.size(o)>0)
512 o=LazyList.get(o,0);
513
514 if (o instanceof byte[])
515 {
516 try
517 {
518 return getParameterBytesAsString(name, (byte[])o);
519 }
520 catch(Exception e)
521 {
522 LOG.warn(e);
523 }
524 }
525 else if (o!=null)
526 return String.valueOf(o);
527 return null;
528 }
529
530
531
532
533
534 @Override
535 public Map getParameterMap()
536 {
537 Map<String, String[]> cmap = new HashMap<String,String[]>();
538
539 for ( Object key : _params.keySet() )
540 {
541 cmap.put((String)key,getParameterValues((String)key));
542 }
543
544 return Collections.unmodifiableMap(cmap);
545 }
546
547
548
549
550
551 @Override
552 public Enumeration getParameterNames()
553 {
554 return Collections.enumeration(_params.keySet());
555 }
556
557
558
559
560
561 @Override
562 public String[] getParameterValues(String name)
563 {
564 List l=_params.getValues(name);
565 if (l==null || l.size()==0)
566 return new String[0];
567 String[] v = new String[l.size()];
568 for (int i=0;i<l.size();i++)
569 {
570 Object o=l.get(i);
571 if (o instanceof byte[])
572 {
573 try
574 {
575 v[i]=getParameterBytesAsString(name, (byte[])o);
576 }
577 catch(Exception e)
578 {
579 throw new RuntimeException(e);
580 }
581 }
582 else if (o instanceof String)
583 v[i]=(String)o;
584 }
585 return v;
586 }
587
588
589
590
591
592 @Override
593 public void setCharacterEncoding(String enc)
594 throws UnsupportedEncodingException
595 {
596 _encoding=enc;
597 }
598
599
600
601 private String getParameterBytesAsString (String name, byte[] bytes)
602 throws UnsupportedEncodingException
603 {
604
605 Object ct = _params.get(name+CONTENT_TYPE_SUFFIX);
606
607 String contentType = _encoding;
608 if (ct != null)
609 {
610 String tmp = MimeTypes.getCharsetFromContentType(new ByteArrayBuffer((String)ct));
611 contentType = (tmp == null?_encoding:tmp);
612 }
613
614 return new String(bytes,contentType);
615 }
616 }
617
618 private static class Base64InputStream extends InputStream
619 {
620 ReadLineInputStream _in;
621 String _line;
622 byte[] _buffer;
623 int _pos;
624
625 public Base64InputStream (ReadLineInputStream in)
626 {
627 _in = in;
628 }
629
630 @Override
631 public int read() throws IOException
632 {
633 if (_buffer==null || _pos>= _buffer.length)
634 {
635 _line = _in.readLine();
636 System.err.println("LINE: "+_line);
637 if (_line==null)
638 return -1;
639 if (_line.startsWith("--"))
640 _buffer=(_line+"\r\n").getBytes();
641 else if (_line.length()==0)
642 _buffer="\r\n".getBytes();
643 else
644 {
645 ByteArrayOutputStream bout = new ByteArrayOutputStream(4*_line.length()/3);
646 B64Code.decode(_line, bout);
647 bout.write(13);
648 bout.write(10);
649 _buffer = bout.toByteArray();
650 }
651
652 _pos=0;
653 }
654 return _buffer[_pos++];
655 }
656 }
657 }