1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.util;
20
21 import java.io.BufferedInputStream;
22 import java.io.BufferedOutputStream;
23 import java.io.ByteArrayInputStream;
24 import java.io.ByteArrayOutputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.FilterInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Locale;
37
38 import javax.servlet.MultipartConfigElement;
39 import javax.servlet.ServletException;
40 import javax.servlet.http.Part;
41
42 import org.eclipse.jetty.util.log.Log;
43 import org.eclipse.jetty.util.log.Logger;
44
45
46
47
48
49
50
51
52 public class MultiPartInputStreamParser
53 {
54 private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.class);
55 public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
56 protected InputStream _in;
57 protected MultipartConfigElement _config;
58 protected String _contentType;
59 protected MultiMap _parts;
60 protected File _tmpDir;
61 protected File _contextTmpDir;
62 protected boolean _deleteOnExit;
63
64
65
66 public class MultiPart implements Part
67 {
68 protected String _name;
69 protected String _filename;
70 protected File _file;
71 protected OutputStream _out;
72 protected ByteArrayOutputStream2 _bout;
73 protected String _contentType;
74 protected MultiMap _headers;
75 protected long _size = 0;
76 protected boolean _temporary = true;
77
78 public MultiPart (String name, String filename)
79 throws IOException
80 {
81 _name = name;
82 _filename = filename;
83 }
84
85 protected void setContentType (String contentType)
86 {
87 _contentType = contentType;
88 }
89
90
91 protected void open()
92 throws IOException
93 {
94
95
96
97 if (_filename != null && _filename.trim().length() > 0)
98 {
99 createFile();
100 }
101 else
102 {
103
104
105 _out = _bout= new ByteArrayOutputStream2();
106 }
107 }
108
109 protected void close()
110 throws IOException
111 {
112 _out.close();
113 }
114
115
116 protected void write (int b)
117 throws IOException
118 {
119 if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getMaxFileSize())
120 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
121
122 if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null)
123 createFile();
124 _out.write(b);
125 _size ++;
126 }
127
128 protected void write (byte[] bytes, int offset, int length)
129 throws IOException
130 {
131 if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStreamParser.this._config.getMaxFileSize())
132 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
133
134 if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null)
135 createFile();
136
137 _out.write(bytes, offset, length);
138 _size += length;
139 }
140
141 protected void createFile ()
142 throws IOException
143 {
144 _file = File.createTempFile("MultiPart", "", MultiPartInputStreamParser.this._tmpDir);
145 if (_deleteOnExit)
146 _file.deleteOnExit();
147 FileOutputStream fos = new FileOutputStream(_file);
148 BufferedOutputStream bos = new BufferedOutputStream(fos);
149
150 if (_size > 0 && _out != null)
151 {
152
153 _out.flush();
154 _bout.writeTo(bos);
155 _out.close();
156 _bout = null;
157 }
158 _out = bos;
159 }
160
161
162
163 protected void setHeaders(MultiMap headers)
164 {
165 _headers = headers;
166 }
167
168
169
170
171 public String getContentType()
172 {
173 return _contentType;
174 }
175
176
177
178
179 public String getHeader(String name)
180 {
181 if (name == null)
182 return null;
183 return (String)_headers.getValue(name.toLowerCase(Locale.ENGLISH), 0);
184 }
185
186
187
188
189 public Collection<String> getHeaderNames()
190 {
191 return _headers.keySet();
192 }
193
194
195
196
197 public Collection<String> getHeaders(String name)
198 {
199 return _headers.getValues(name);
200 }
201
202
203
204
205 public InputStream getInputStream() throws IOException
206 {
207 if (_file != null)
208 {
209
210 return new BufferedInputStream (new FileInputStream(_file));
211 }
212 else
213 {
214
215 return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
216 }
217 }
218
219
220
221
222
223 @Override
224 public String getSubmittedFileName()
225 {
226 return getContentDispositionFilename();
227 }
228
229 public byte[] getBytes()
230 {
231 if (_bout!=null)
232 return _bout.toByteArray();
233 return null;
234 }
235
236
237
238
239 public String getName()
240 {
241 return _name;
242 }
243
244
245
246
247 public long getSize()
248 {
249 return _size;
250 }
251
252
253
254
255 public void write(String fileName) throws IOException
256 {
257 if (_file == null)
258 {
259 _temporary = false;
260
261
262 _file = new File (_tmpDir, fileName);
263
264 BufferedOutputStream bos = null;
265 try
266 {
267 bos = new BufferedOutputStream(new FileOutputStream(_file));
268 _bout.writeTo(bos);
269 bos.flush();
270 }
271 finally
272 {
273 if (bos != null)
274 bos.close();
275 _bout = null;
276 }
277 }
278 else
279 {
280
281 _temporary = false;
282
283 File f = new File(_tmpDir, fileName);
284 if (_file.renameTo(f))
285 _file = f;
286 }
287 }
288
289
290
291
292
293
294 public void delete() throws IOException
295 {
296 if (_file != null && _file.exists())
297 _file.delete();
298 }
299
300
301
302
303
304
305 public void cleanUp() throws IOException
306 {
307 if (_temporary && _file != null && _file.exists())
308 _file.delete();
309 }
310
311
312
313
314
315 public File getFile ()
316 {
317 return _file;
318 }
319
320
321
322
323
324
325 public String getContentDispositionFilename ()
326 {
327 return _filename;
328 }
329 }
330
331
332
333
334
335
336
337
338
339
340 public MultiPartInputStreamParser (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
341 {
342 _in = new ReadLineInputStream(in);
343 _contentType = contentType;
344 _config = config;
345 _contextTmpDir = contextTmpDir;
346 if (_contextTmpDir == null)
347 _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
348
349 if (_config == null)
350 _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
351 }
352
353
354
355
356 public Collection<Part> getParsedParts()
357 {
358 if (_parts == null)
359 return Collections.emptyList();
360
361 Collection<Object> values = _parts.values();
362 List<Part> parts = new ArrayList<Part>();
363 for (Object o: values)
364 {
365 List<Part> asList = LazyList.getList(o, false);
366 parts.addAll(asList);
367 }
368 return parts;
369 }
370
371
372
373
374
375
376 public void deleteParts ()
377 throws MultiException
378 {
379 Collection<Part> parts = getParsedParts();
380 MultiException err = new MultiException();
381 for (Part p:parts)
382 {
383 try
384 {
385 ((MultiPartInputStreamParser.MultiPart)p).cleanUp();
386 }
387 catch(Exception e)
388 {
389 err.add(e);
390 }
391 }
392 _parts.clear();
393
394 err.ifExceptionThrowMulti();
395 }
396
397
398
399
400
401
402
403
404 public Collection<Part> getParts()
405 throws IOException, ServletException
406 {
407 parse();
408 Collection<Object> values = _parts.values();
409 List<Part> parts = new ArrayList<Part>();
410 for (Object o: values)
411 {
412 List<Part> asList = LazyList.getList(o, false);
413 parts.addAll(asList);
414 }
415 return parts;
416 }
417
418
419
420
421
422
423
424
425
426 public Part getPart(String name)
427 throws IOException, ServletException
428 {
429 parse();
430 return (Part)_parts.getValue(name, 0);
431 }
432
433
434
435
436
437
438
439
440 protected void parse ()
441 throws IOException, ServletException
442 {
443
444 if (_parts != null)
445 return;
446
447
448 long total = 0;
449 _parts = new MultiMap();
450
451
452 if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
453 return;
454
455
456
457 if (_config.getLocation() == null)
458 _tmpDir = _contextTmpDir;
459 else if ("".equals(_config.getLocation()))
460 _tmpDir = _contextTmpDir;
461 else
462 {
463 File f = new File (_config.getLocation());
464 if (f.isAbsolute())
465 _tmpDir = f;
466 else
467 _tmpDir = new File (_contextTmpDir, _config.getLocation());
468 }
469
470 if (!_tmpDir.exists())
471 _tmpDir.mkdirs();
472
473 String contentTypeBoundary = "";
474 int bstart = _contentType.indexOf("boundary=");
475 if (bstart >= 0)
476 {
477 int bend = _contentType.indexOf(";", bstart);
478 bend = (bend < 0? _contentType.length(): bend);
479 contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
480 }
481
482 String boundary="--"+contentTypeBoundary;
483 byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
484
485
486 String line = null;
487 try
488 {
489 line=((ReadLineInputStream)_in).readLine();
490 }
491 catch (IOException e)
492 {
493 LOG.warn("Badly formatted multipart request");
494 throw e;
495 }
496
497 if (line == null)
498 throw new IOException("Missing content for multipart request");
499
500 boolean badFormatLogged = false;
501 line=line.trim();
502 while (line != null && !line.equals(boundary))
503 {
504 if (!badFormatLogged)
505 {
506 LOG.warn("Badly formatted multipart request");
507 badFormatLogged = true;
508 }
509 line=((ReadLineInputStream)_in).readLine();
510 line=(line==null?line:line.trim());
511 }
512
513 if (line == null)
514 throw new IOException("Missing initial multi part boundary");
515
516
517 boolean lastPart=false;
518
519 outer:while(!lastPart)
520 {
521 String contentDisposition=null;
522 String contentType=null;
523 String contentTransferEncoding=null;
524
525 MultiMap headers = new MultiMap();
526 while(true)
527 {
528 line=((ReadLineInputStream)_in).readLine();
529
530
531 if(line==null)
532 break outer;
533
534
535 if("".equals(line))
536 break;
537
538 total += line.length();
539 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
540 throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
541
542
543 int c=line.indexOf(':',0);
544 if(c>0)
545 {
546 String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
547 String value=line.substring(c+1,line.length()).trim();
548 headers.put(key, value);
549 if (key.equalsIgnoreCase("content-disposition"))
550 contentDisposition=value;
551 if (key.equalsIgnoreCase("content-type"))
552 contentType = value;
553 if(key.equals("content-transfer-encoding"))
554 contentTransferEncoding=value;
555 }
556 }
557
558
559 boolean form_data=false;
560 if(contentDisposition==null)
561 {
562 throw new IOException("Missing content-disposition");
563 }
564
565 QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true);
566 String name=null;
567 String filename=null;
568 while(tok.hasMoreTokens())
569 {
570 String t=tok.nextToken().trim();
571 String tl=t.toLowerCase(Locale.ENGLISH);
572 if(t.startsWith("form-data"))
573 form_data=true;
574 else if(tl.startsWith("name="))
575 name=value(t);
576 else if(tl.startsWith("filename="))
577 filename=filenameValue(t);
578 }
579
580
581 if(!form_data)
582 {
583 continue;
584 }
585
586
587
588
589
590 if(name==null)
591 {
592 continue;
593 }
594
595
596 MultiPart part = new MultiPart(name, filename);
597 part.setHeaders(headers);
598 part.setContentType(contentType);
599 _parts.add(name, part);
600 part.open();
601
602 InputStream partInput = null;
603 if ("base64".equalsIgnoreCase(contentTransferEncoding))
604 {
605 partInput = new Base64InputStream((ReadLineInputStream)_in);
606 }
607 else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
608 {
609 partInput = new FilterInputStream(_in)
610 {
611 @Override
612 public int read() throws IOException
613 {
614 int c = in.read();
615 if (c >= 0 && c == '=')
616 {
617 int hi = in.read();
618 int lo = in.read();
619 if (hi < 0 || lo < 0)
620 {
621 throw new IOException("Unexpected end to quoted-printable byte");
622 }
623 char[] chars = new char[] { (char)hi, (char)lo };
624 c = Integer.parseInt(new String(chars),16);
625 }
626 return c;
627 }
628 };
629 }
630 else
631 partInput = _in;
632
633
634 try
635 {
636 int state=-2;
637 int c;
638 boolean cr=false;
639 boolean lf=false;
640
641
642 while(true)
643 {
644 int b=0;
645 while((c=(state!=-2)?state:partInput.read())!=-1)
646 {
647 total ++;
648 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
649 throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
650
651 state=-2;
652
653
654 if(c==13||c==10)
655 {
656 if(c==13)
657 {
658 partInput.mark(1);
659 int tmp=partInput.read();
660 if (tmp!=10)
661 partInput.reset();
662 else
663 state=tmp;
664 }
665 break;
666 }
667
668
669 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
670 {
671 b++;
672 }
673 else
674 {
675
676
677 if(cr)
678 part.write(13);
679
680 if(lf)
681 part.write(10);
682
683 cr=lf=false;
684 if(b>0)
685 part.write(byteBoundary,0,b);
686
687 b=-1;
688 part.write(c);
689 }
690 }
691
692
693 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
694 {
695 if(cr)
696 part.write(13);
697
698 if(lf)
699 part.write(10);
700
701 cr=lf=false;
702 part.write(byteBoundary,0,b);
703 b=-1;
704 }
705
706
707 if(b>0||c==-1)
708 {
709
710 if(b==byteBoundary.length)
711 lastPart=true;
712 if(state==10)
713 state=-2;
714 break;
715 }
716
717
718 if(cr)
719 part.write(13);
720
721 if(lf)
722 part.write(10);
723
724 cr=(c==13);
725 lf=(c==10||state==10);
726 if(state==10)
727 state=-2;
728 }
729 }
730 finally
731 {
732
733 part.close();
734 }
735 }
736 if (!lastPart)
737 throw new IOException("Incomplete parts");
738 }
739
740 public void setDeleteOnExit(boolean deleteOnExit)
741 {
742 _deleteOnExit = deleteOnExit;
743 }
744
745
746 public boolean isDeleteOnExit()
747 {
748 return _deleteOnExit;
749 }
750
751
752
753 private String value(String nameEqualsValue)
754 {
755 int idx = nameEqualsValue.indexOf('=');
756 String value = nameEqualsValue.substring(idx+1).trim();
757 return QuotedStringTokenizer.unquoteOnly(value);
758 }
759
760
761
762 private String filenameValue(String nameEqualsValue)
763 {
764 int idx = nameEqualsValue.indexOf('=');
765 String value = nameEqualsValue.substring(idx+1).trim();
766
767 if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
768 {
769
770
771 char first=value.charAt(0);
772 if (first=='"' || first=='\'')
773 value=value.substring(1);
774 char last=value.charAt(value.length()-1);
775 if (last=='"' || last=='\'')
776 value = value.substring(0,value.length()-1);
777
778 return value;
779 }
780 else
781
782
783
784
785 return QuotedStringTokenizer.unquoteOnly(value, true);
786 }
787
788
789
790 private static class Base64InputStream extends InputStream
791 {
792 ReadLineInputStream _in;
793 String _line;
794 byte[] _buffer;
795 int _pos;
796
797
798 public Base64InputStream(ReadLineInputStream rlis)
799 {
800 _in = rlis;
801 }
802
803 @Override
804 public int read() throws IOException
805 {
806 if (_buffer==null || _pos>= _buffer.length)
807 {
808
809
810
811
812 _line = _in.readLine();
813 if (_line==null)
814 return -1;
815 if (_line.startsWith("--"))
816 _buffer=(_line+"\r\n").getBytes();
817 else if (_line.length()==0)
818 _buffer="\r\n".getBytes();
819 else
820 {
821 ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2);
822 B64Code.decode(_line, baos);
823 baos.write(13);
824 baos.write(10);
825 _buffer = baos.toByteArray();
826 }
827
828 _pos=0;
829 }
830
831 return _buffer[_pos++];
832 }
833 }
834 }