1 // 2 // ======================================================================== 3 // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. 4 // ------------------------------------------------------------------------ 5 // All rights reserved. This program and the accompanying materials 6 // are made available under the terms of the Eclipse Public License v1.0 7 // and Apache License v2.0 which accompanies this distribution. 8 // 9 // The Eclipse Public License is available at 10 // http://www.eclipse.org/legal/epl-v10.html 11 // 12 // The Apache License v2.0 is available at 13 // http://www.opensource.org/licenses/apache2.0.php 14 // 15 // You may elect to redistribute this code under either of these licenses. 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 * MultiPartInputStream 56 * 57 * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings. 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 MultiPart(String name, String filename)86 public MultiPart (String name, String filename) 87 throws IOException 88 { 89 _name = name; 90 _filename = filename; 91 } 92 setContentType(String contentType)93 protected void setContentType (String contentType) 94 { 95 _contentType = contentType; 96 } 97 98 open()99 protected void open() 100 throws IOException 101 { 102 //We will either be writing to a file, if it has a filename on the content-disposition 103 //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we 104 //will need to change to write to a file. 105 if (_filename != null && _filename.trim().length() > 0) 106 { 107 createFile(); 108 } 109 else 110 { 111 //Write to a buffer in memory until we discover we've exceed the 112 //MultipartConfig fileSizeThreshold 113 _out = _bout= new ByteArrayOutputStream2(); 114 } 115 } 116 close()117 protected void close() 118 throws IOException 119 { 120 _out.close(); 121 } 122 123 write(int b)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 write(byte[] bytes, int offset, int length)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 createFile()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 //already written some bytes, so need to copy them into the file 161 _out.flush(); 162 _bout.writeTo(bos); 163 _out.close(); 164 _bout = null; 165 } 166 _out = bos; 167 } 168 169 170 setHeaders(MultiMap<String> headers)171 protected void setHeaders(MultiMap<String> headers) 172 { 173 _headers = headers; 174 } 175 176 /** 177 * @see javax.servlet.http.Part#getContentType() 178 */ getContentType()179 public String getContentType() 180 { 181 return _contentType; 182 } 183 184 /** 185 * @see javax.servlet.http.Part#getHeader(java.lang.String) 186 */ getHeader(String name)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 * @see javax.servlet.http.Part#getHeaderNames() 196 */ getHeaderNames()197 public Collection<String> getHeaderNames() 198 { 199 return _headers.keySet(); 200 } 201 202 /** 203 * @see javax.servlet.http.Part#getHeaders(java.lang.String) 204 */ getHeaders(String name)205 public Collection<String> getHeaders(String name) 206 { 207 return _headers.getValues(name); 208 } 209 210 /** 211 * @see javax.servlet.http.Part#getInputStream() 212 */ getInputStream()213 public InputStream getInputStream() throws IOException 214 { 215 if (_file != null) 216 { 217 //written to a file, whether temporary or not 218 return new BufferedInputStream (new FileInputStream(_file)); 219 } 220 else 221 { 222 //part content is in memory 223 return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size()); 224 } 225 } 226 getBytes()227 public byte[] getBytes() 228 { 229 if (_bout!=null) 230 return _bout.toByteArray(); 231 return null; 232 } 233 234 /** 235 * @see javax.servlet.http.Part#getName() 236 */ getName()237 public String getName() 238 { 239 return _name; 240 } 241 242 /** 243 * @see javax.servlet.http.Part#getSize() 244 */ getSize()245 public long getSize() 246 { 247 return _size; 248 } 249 250 /** 251 * @see javax.servlet.http.Part#write(java.lang.String) 252 */ write(String fileName)253 public void write(String fileName) throws IOException 254 { 255 if (_file == null) 256 { 257 _temporary = false; 258 259 //part data is only in the ByteArrayOutputStream and never been written to disk 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 //the part data is already written to a temporary file, just rename it 279 _temporary = false; 280 281 File f = new File(_tmpDir, fileName); 282 if (_file.renameTo(f)) 283 _file = f; 284 } 285 } 286 287 /** 288 * Remove the file, whether or not Part.write() was called on it 289 * (ie no longer temporary) 290 * @see javax.servlet.http.Part#delete() 291 */ delete()292 public void delete() throws IOException 293 { 294 if (_file != null && _file.exists()) 295 _file.delete(); 296 } 297 298 /** 299 * Only remove tmp files. 300 * 301 * @throws IOException 302 */ cleanUp()303 public void cleanUp() throws IOException 304 { 305 if (_temporary && _file != null && _file.exists()) 306 _file.delete(); 307 } 308 309 310 /** 311 * Get the file, if any, the data has been written to. 312 * @return 313 */ getFile()314 public File getFile () 315 { 316 return _file; 317 } 318 319 320 /** 321 * Get the filename from the content-disposition. 322 * @return null or the filename 323 */ getContentDispositionFilename()324 public String getContentDispositionFilename () 325 { 326 return _filename; 327 } 328 } 329 330 331 332 333 /** 334 * @param in Request input stream 335 * @param contentType Content-Type header 336 * @param config MultipartConfigElement 337 * @param contextTmpDir javax.servlet.context.tempdir 338 */ MultiPartInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)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 * Get the already parsed parts. 354 * 355 * @return 356 */ getParsedParts()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 * Delete any tmp storage for parts, and clear out the parts list. 374 * 375 * @throws MultiException 376 */ deleteParts()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 * Parse, if necessary, the multipart data and return the list of Parts. 401 * 402 * @return 403 * @throws IOException 404 * @throws ServletException 405 */ getParts()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 * Get the named Part. 423 * 424 * @param name 425 * @return 426 * @throws IOException 427 * @throws ServletException 428 */ getPart(String name)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 * Parse, if necessary, the multipart stream. 439 * 440 * @throws IOException 441 * @throws ServletException 442 */ parse()443 protected void parse () 444 throws IOException, ServletException 445 { 446 //have we already parsed the input? 447 if (_parts != null) 448 return; 449 450 //initialize 451 long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize 452 _parts = new MultiMap<String>(); 453 454 //if its not a multipart request, don't parse it 455 if (_contentType == null || !_contentType.startsWith("multipart/form-data")) 456 return; 457 458 //sort out the location to which to write the files 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 // Get first boundary 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 // Read each part 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 //No more input 534 if(line==null) 535 break outer; 536 537 // If blank line, end of part headers 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 //get content-disposition and content-type 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 // Extract content-disposition 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 // Check disposition 585 if(!form_data) 586 { 587 continue; 588 } 589 //It is valid for reset and submit buttons to have an empty name. 590 //If no name is supplied, the browser skips sending the info for that field. 591 //However, if you supply the empty string as the name, the browser sends the 592 //field, with name as the empty string. So, only continue this loop if we 593 //have not yet seen a name field. 594 if(name==null) 595 { 596 continue; 597 } 598 599 //Have a new Part 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 // loop for all lines 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 // look for CR and/or LF 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 // Look for boundary 672 if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b]) 673 { 674 b++; 675 } 676 else 677 { 678 // Got a character not part of the boundary, so we don't have the boundary marker. 679 // Write out as many chars as we matched, then the char we're looking at. 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 // Check for incomplete boundary match, writing out the chars we matched along the way 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 // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part. 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 // handle CR LF 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 setDeleteOnExit(boolean deleteOnExit)742 public void setDeleteOnExit(boolean deleteOnExit) 743 { 744 _deleteOnExit = deleteOnExit; 745 } 746 747 isDeleteOnExit()748 public boolean isDeleteOnExit() 749 { 750 return _deleteOnExit; 751 } 752 753 754 /* ------------------------------------------------------------ */ value(String nameEqualsValue, boolean splitAfterSpace)755 private String value(String nameEqualsValue, boolean splitAfterSpace) 756 { 757 /* 758 String value=nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim(); 759 int i=value.indexOf(';'); 760 if(i>0) 761 value=value.substring(0,i); 762 if(value.startsWith("\"")) 763 { 764 value=value.substring(1,value.indexOf('"',1)); 765 } 766 else if (splitAfterSpace) 767 { 768 i=value.indexOf(' '); 769 if(i>0) 770 value=value.substring(0,i); 771 } 772 return value; 773 */ 774 int idx = nameEqualsValue.indexOf('='); 775 String value = nameEqualsValue.substring(idx+1).trim(); 776 return QuotedStringTokenizer.unquoteOnly(value); 777 } 778 779 780 /* ------------------------------------------------------------ */ filenameValue(String nameEqualsValue)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 //incorrectly escaped IE filenames that have the whole path 789 //we just strip any leading & trailing quotes and leave it as is 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 //unquote the string, but allow any backslashes that don't 801 //form a valid escape sequence to remain as many browsers 802 //even on *nix systems will not escape a filename containing 803 //backslashes 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 Base64InputStream(ReadLineInputStream rlis)815 public Base64InputStream(ReadLineInputStream rlis) 816 { 817 _in = rlis; 818 } 819 820 @Override read()821 public int read() throws IOException 822 { 823 if (_buffer==null || _pos>= _buffer.length) 824 { 825 //Any CR and LF will be consumed by the readLine() call. 826 //We need to put them back into the bytes returned from this 827 //method because the parsing of the multipart content uses them 828 //as markers to determine when we've reached the end of a part. 829 _line = _in.readLine(); 830 if (_line==null) 831 return -1; //nothing left 832 if (_line.startsWith("--")) 833 _buffer=(_line+"\r\n").getBytes(); //boundary marking end of part 834 else if (_line.length()==0) 835 _buffer="\r\n".getBytes(); //blank line 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 } 852