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