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