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 public byte[] getBytes()
220 {
221 if (_bout!=null)
222 return _bout.toByteArray();
223 return null;
224 }
225
226
227
228
229 public String getName()
230 {
231 return _name;
232 }
233
234
235
236
237 public long getSize()
238 {
239 return _size;
240 }
241
242
243
244
245 public void write(String fileName) throws IOException
246 {
247 if (_file == null)
248 {
249 _temporary = false;
250
251
252 _file = new File (_tmpDir, fileName);
253
254 BufferedOutputStream bos = null;
255 try
256 {
257 bos = new BufferedOutputStream(new FileOutputStream(_file));
258 _bout.writeTo(bos);
259 bos.flush();
260 }
261 finally
262 {
263 if (bos != null)
264 bos.close();
265 _bout = null;
266 }
267 }
268 else
269 {
270
271 _temporary = false;
272
273 File f = new File(_tmpDir, fileName);
274 if (_file.renameTo(f))
275 _file = f;
276 }
277 }
278
279
280
281
282
283
284 public void delete() throws IOException
285 {
286 if (_file != null && _file.exists())
287 _file.delete();
288 }
289
290
291
292
293
294
295 public void cleanUp() throws IOException
296 {
297 if (_temporary && _file != null && _file.exists())
298 _file.delete();
299 }
300
301
302
303
304
305
306 public File getFile ()
307 {
308 return _file;
309 }
310
311
312
313
314
315
316 public String getContentDispositionFilename ()
317 {
318 return _filename;
319 }
320 }
321
322
323
324
325
326
327
328
329
330
331 public MultiPartInputStreamParser (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
332 {
333 _in = new ReadLineInputStream(in);
334 _contentType = contentType;
335 _config = config;
336 _contextTmpDir = contextTmpDir;
337 if (_contextTmpDir == null)
338 _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
339
340 if (_config == null)
341 _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
342 }
343
344
345
346
347
348
349 public Collection<Part> getParsedParts()
350 {
351 if (_parts == null)
352 return Collections.emptyList();
353
354 Collection<Object> values = _parts.values();
355 List<Part> parts = new ArrayList<Part>();
356 for (Object o: values)
357 {
358 List<Part> asList = LazyList.getList(o, false);
359 parts.addAll(asList);
360 }
361 return parts;
362 }
363
364
365
366
367
368
369 public void deleteParts ()
370 throws MultiException
371 {
372 Collection<Part> parts = getParsedParts();
373 MultiException err = new MultiException();
374 for (Part p:parts)
375 {
376 try
377 {
378 ((MultiPartInputStreamParser.MultiPart)p).cleanUp();
379 }
380 catch(Exception e)
381 {
382 err.add(e);
383 }
384 }
385 _parts.clear();
386
387 err.ifExceptionThrowMulti();
388 }
389
390
391
392
393
394
395
396
397
398 public Collection<Part> getParts()
399 throws IOException, ServletException
400 {
401 parse();
402 Collection<Object> values = _parts.values();
403 List<Part> parts = new ArrayList<Part>();
404 for (Object o: values)
405 {
406 List<Part> asList = LazyList.getList(o, false);
407 parts.addAll(asList);
408 }
409 return parts;
410 }
411
412
413
414
415
416
417
418
419
420
421 public Part getPart(String name)
422 throws IOException, ServletException
423 {
424 parse();
425 return (Part)_parts.getValue(name, 0);
426 }
427
428
429
430
431
432
433
434
435 protected void parse ()
436 throws IOException, ServletException
437 {
438
439 if (_parts != null)
440 return;
441
442
443 long total = 0;
444 _parts = new MultiMap();
445
446
447 if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
448 return;
449
450
451
452 if (_config.getLocation() == null)
453 _tmpDir = _contextTmpDir;
454 else if ("".equals(_config.getLocation()))
455 _tmpDir = _contextTmpDir;
456 else
457 {
458 File f = new File (_config.getLocation());
459 if (f.isAbsolute())
460 _tmpDir = f;
461 else
462 _tmpDir = new File (_contextTmpDir, _config.getLocation());
463 }
464
465 if (!_tmpDir.exists())
466 _tmpDir.mkdirs();
467
468 String contentTypeBoundary = "";
469 int bstart = _contentType.indexOf("boundary=");
470 if (bstart >= 0)
471 {
472 int bend = _contentType.indexOf(";", bstart);
473 bend = (bend < 0? _contentType.length(): bend);
474 contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
475 }
476
477 String boundary="--"+contentTypeBoundary;
478 byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1);
479
480
481 String line = null;
482 try
483 {
484 line=((ReadLineInputStream)_in).readLine();
485 }
486 catch (IOException e)
487 {
488 LOG.warn("Badly formatted multipart request");
489 throw e;
490 }
491
492 if (line == null)
493 throw new IOException("Missing content for multipart request");
494
495 boolean badFormatLogged = false;
496 line=line.trim();
497 while (line != null && !line.equals(boundary))
498 {
499 if (!badFormatLogged)
500 {
501 LOG.warn("Badly formatted multipart request");
502 badFormatLogged = true;
503 }
504 line=((ReadLineInputStream)_in).readLine();
505 line=(line==null?line:line.trim());
506 }
507
508 if (line == null)
509 throw new IOException("Missing initial multi part boundary");
510
511
512 boolean lastPart=false;
513
514 outer:while(!lastPart)
515 {
516 String contentDisposition=null;
517 String contentType=null;
518 String contentTransferEncoding=null;
519
520 MultiMap headers = new MultiMap();
521 while(true)
522 {
523 line=((ReadLineInputStream)_in).readLine();
524
525
526 if(line==null)
527 break outer;
528
529
530 if("".equals(line))
531 break;
532
533 total += line.length();
534 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
535 throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
536
537
538 int c=line.indexOf(':',0);
539 if(c>0)
540 {
541 String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
542 String value=line.substring(c+1,line.length()).trim();
543 headers.put(key, value);
544 if (key.equalsIgnoreCase("content-disposition"))
545 contentDisposition=value;
546 if (key.equalsIgnoreCase("content-type"))
547 contentType = value;
548 if(key.equals("content-transfer-encoding"))
549 contentTransferEncoding=value;
550 }
551 }
552
553
554 boolean form_data=false;
555 if(contentDisposition==null)
556 {
557 throw new IOException("Missing content-disposition");
558 }
559
560 QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true);
561 String name=null;
562 String filename=null;
563 while(tok.hasMoreTokens())
564 {
565 String t=tok.nextToken().trim();
566 String tl=t.toLowerCase(Locale.ENGLISH);
567 if(t.startsWith("form-data"))
568 form_data=true;
569 else if(tl.startsWith("name="))
570 name=value(t);
571 else if(tl.startsWith("filename="))
572 filename=filenameValue(t);
573 }
574
575
576 if(!form_data)
577 {
578 continue;
579 }
580
581
582
583
584
585 if(name==null)
586 {
587 continue;
588 }
589
590
591 MultiPart part = new MultiPart(name, filename);
592 part.setHeaders(headers);
593 part.setContentType(contentType);
594 _parts.add(name, part);
595 part.open();
596
597 InputStream partInput = null;
598 if ("base64".equalsIgnoreCase(contentTransferEncoding))
599 {
600 partInput = new Base64InputStream((ReadLineInputStream)_in);
601 }
602 else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
603 {
604 partInput = new FilterInputStream(_in)
605 {
606 @Override
607 public int read() throws IOException
608 {
609 int c = in.read();
610 if (c >= 0 && c == '=')
611 {
612 int hi = in.read();
613 int lo = in.read();
614 if (hi < 0 || lo < 0)
615 {
616 throw new IOException("Unexpected end to quoted-printable byte");
617 }
618 char[] chars = new char[] { (char)hi, (char)lo };
619 c = Integer.parseInt(new String(chars),16);
620 }
621 return c;
622 }
623 };
624 }
625 else
626 partInput = _in;
627
628
629 try
630 {
631 int state=-2;
632 int c;
633 boolean cr=false;
634 boolean lf=false;
635
636
637 while(true)
638 {
639 int b=0;
640 while((c=(state!=-2)?state:partInput.read())!=-1)
641 {
642 total ++;
643 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
644 throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
645
646 state=-2;
647
648
649 if(c==13||c==10)
650 {
651 if(c==13)
652 {
653 partInput.mark(1);
654 int tmp=partInput.read();
655 if (tmp!=10)
656 partInput.reset();
657 else
658 state=tmp;
659 }
660 break;
661 }
662
663
664 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
665 {
666 b++;
667 }
668 else
669 {
670
671
672 if(cr)
673 part.write(13);
674
675 if(lf)
676 part.write(10);
677
678 cr=lf=false;
679 if(b>0)
680 part.write(byteBoundary,0,b);
681
682 b=-1;
683 part.write(c);
684 }
685 }
686
687
688 if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
689 {
690 if(cr)
691 part.write(13);
692
693 if(lf)
694 part.write(10);
695
696 cr=lf=false;
697 part.write(byteBoundary,0,b);
698 b=-1;
699 }
700
701
702 if(b>0||c==-1)
703 {
704
705 if(b==byteBoundary.length)
706 lastPart=true;
707 if(state==10)
708 state=-2;
709 break;
710 }
711
712
713 if(cr)
714 part.write(13);
715
716 if(lf)
717 part.write(10);
718
719 cr=(c==13);
720 lf=(c==10||state==10);
721 if(state==10)
722 state=-2;
723 }
724 }
725 finally
726 {
727
728 part.close();
729 }
730 }
731 if (!lastPart)
732 throw new IOException("Incomplete parts");
733 }
734
735 public void setDeleteOnExit(boolean deleteOnExit)
736 {
737 _deleteOnExit = deleteOnExit;
738 }
739
740
741 public boolean isDeleteOnExit()
742 {
743 return _deleteOnExit;
744 }
745
746
747
748 private String value(String nameEqualsValue)
749 {
750 int idx = nameEqualsValue.indexOf('=');
751 String value = nameEqualsValue.substring(idx+1).trim();
752 return QuotedStringTokenizer.unquoteOnly(value);
753 }
754
755
756
757 private String filenameValue(String nameEqualsValue)
758 {
759 int idx = nameEqualsValue.indexOf('=');
760 String value = nameEqualsValue.substring(idx+1).trim();
761
762 if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
763 {
764
765
766 char first=value.charAt(0);
767 if (first=='"' || first=='\'')
768 value=value.substring(1);
769 char last=value.charAt(value.length()-1);
770 if (last=='"' || last=='\'')
771 value = value.substring(0,value.length()-1);
772
773 return value;
774 }
775 else
776
777
778
779
780 return QuotedStringTokenizer.unquoteOnly(value, true);
781 }
782
783
784
785 private static class Base64InputStream extends InputStream
786 {
787 ReadLineInputStream _in;
788 String _line;
789 byte[] _buffer;
790 int _pos;
791
792
793 public Base64InputStream(ReadLineInputStream rlis)
794 {
795 _in = rlis;
796 }
797
798 @Override
799 public int read() throws IOException
800 {
801 if (_buffer==null || _pos>= _buffer.length)
802 {
803
804
805
806
807 _line = _in.readLine();
808 if (_line==null)
809 return -1;
810 if (_line.startsWith("--"))
811 _buffer=(_line+"\r\n").getBytes();
812 else if (_line.length()==0)
813 _buffer="\r\n".getBytes();
814 else
815 {
816 ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2);
817 B64Code.decode(_line, baos);
818 baos.write(13);
819 baos.write(10);
820 _buffer = baos.toByteArray();
821 }
822
823 _pos=0;
824 }
825
826 return _buffer[_pos++];
827 }
828 }
829 }