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