1 module hunt.http.codec.http.model.MultipartFormInputStream; 2 3 import hunt.http.codec.http.model.MultiException; 4 import hunt.http.codec.http.model.MultipartConfig; 5 import hunt.http.codec.http.model.MultipartParser; 6 7 import hunt.container; 8 import hunt.io; 9 import hunt.lang.exception; 10 import hunt.logging; 11 import hunt.string; 12 13 import std.array; 14 import std.conv; 15 import std.file; 16 import std.path; 17 import std.regex; 18 import std.string; 19 import std.uni; 20 21 22 void deleteOnExit(string file) { 23 if(file.exists) { 24 version(HUNT_DEBUG) infof("File removed: %s", file); 25 file.remove(); 26 } else { 27 warningf("File not exists: %s", file); 28 } 29 } 30 31 /** 32 * MultiPartInputStream 33 * <p> 34 * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings. 35 * 36 * @see <a href="https://tools.ietf.org/html/rfc7578">https://tools.ietf.org/html/rfc7578</a> 37 */ 38 class MultipartFormInputStream { 39 40 private int _bufferSize = 16 * 1024; 41 __gshared MultipartConfig __DEFAULT_MULTIPART_CONFIG; 42 __gshared MultiMap!(Part) EMPTY_MAP; 43 protected InputStream _in; 44 protected MultipartConfig _config; 45 protected string _contentType; 46 protected MultiMap!(Part) _parts; 47 protected Exception _err; 48 protected string _tmpDir; 49 protected string _contextTmpDir; 50 protected bool _deleteOnExit; 51 protected bool _writeFilesWithFilenames; 52 protected bool _parsed; 53 54 shared static this() { 55 __DEFAULT_MULTIPART_CONFIG = new MultipartConfig(tempDir()); 56 EMPTY_MAP = new MultiMap!(Part)(Collections.emptyMap!(string, List!(Part))()); 57 } 58 59 class MultiPart : Part { 60 protected string _name; 61 protected string _filename; 62 protected string _file; 63 protected OutputStream _out; 64 protected ByteArrayOutputStream _bout; 65 protected string _contentType; 66 protected MultiMap!string _headers; 67 protected long _size = 0; 68 protected bool _temporary = true; 69 private bool isWriteToFile = false; 70 71 this(string name, string filename) { 72 _name = name; 73 _filename = filename; 74 } 75 76 override 77 string toString() { 78 return format("Part{n=%s,fn=%s,ct=%s,s=%d,tmp=%b,file=%s}", 79 _name, _filename, _contentType, _size, _temporary, _file); 80 } 81 82 protected void setContentType(string contentType) { 83 _contentType = contentType; 84 } 85 86 protected void open() { 87 // We will either be writing to a file, if it has a filename on the content-disposition 88 // and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we 89 // will need to change to write to a file. 90 if (isWriteFilesWithFilenames() && !_filename.empty) { 91 createFile(); 92 } else { 93 // Write to a buffer in memory until we discover we've exceed the 94 // MultipartConfig fileSizeThreshold 95 _out = _bout = new ByteArrayOutputStream(); 96 } 97 } 98 99 protected void close() { 100 _out.close(); 101 } 102 103 protected void write(int b) { 104 if (this.outer._config.getMaxFileSize() > 0 && _size + 1 > this.outer._config.getMaxFileSize()) 105 throw new IllegalStateException("Multipart Mime part " ~ _name ~ " exceeds max filesize"); 106 107 if (this.outer._config.getFileSizeThreshold() > 0 && 108 _size + 1 > this.outer._config.getFileSizeThreshold() && _file is null) { 109 createFile(); 110 } 111 _out.write(b); 112 _size++; 113 } 114 115 protected void write(byte[] bytes, int offset, int length) { 116 if (this.outer._config.getMaxFileSize() > 0 && _size + length > this.outer._config.getMaxFileSize()) 117 throw new IllegalStateException("Multipart Mime part " ~ _name ~ " exceeds max filesize"); 118 119 if (this.outer._config.getFileSizeThreshold() > 0 120 && _size + length > this.outer._config.getFileSizeThreshold() && _file is null) { 121 createFile(); 122 } 123 _out.write(bytes, offset, length); 124 _size += length; 125 } 126 127 protected void createFile() { 128 /* 129 * Some statics just to make the code below easier to understand This get optimized away during the compile anyway 130 */ 131 // bool USER = true; 132 // bool WORLD = false; 133 _file= buildPath(this.outer._tmpDir, "MultiPart-" ~ StringUtils.randomId()); 134 version(HUNT_DEBUG) trace("Creating temp file: ", _file); 135 136 // _file = File.createTempFile("MultiPart", "", this.outer._tmpDir); 137 // _file.setReadable(false, WORLD); // (reset) disable it for everyone first 138 // _file.setReadable(true, USER); // enable for user only 139 140 // if (_deleteOnExit) 141 // _file.deleteOnExit(); 142 FileOutputStream fos = new FileOutputStream(_file); 143 BufferedOutputStream bos = new BufferedOutputStream(fos); 144 145 if (_size > 0 && _out !is null) { 146 // already written some bytes, so need to copy them into the file 147 _out.flush(); 148 _bout.writeTo(bos); 149 _out.close(); 150 } 151 _bout = null; 152 _out = bos; 153 isWriteToFile = true; 154 } 155 156 protected void setHeaders(MultiMap!string headers) { 157 _headers = headers; 158 } 159 160 /** 161 * @see Part#getContentType() 162 */ 163 override 164 string getContentType() { 165 return _contentType; 166 } 167 168 /** 169 * @see Part#getHeader(string) 170 */ 171 override 172 string getHeader(string name) { 173 if (name is null) 174 return null; 175 return _headers.getValue(std.uni.toLower(name), 0); 176 } 177 178 /** 179 * @see Part#getHeaderNames() 180 */ 181 // override 182 string[] getHeaderNames() { 183 return _headers.keySet(); 184 } 185 186 /** 187 * @see Part#getHeaders(string) 188 */ 189 override 190 Collection!string getHeaders(string name) { 191 return _headers.getValues(name); 192 } 193 194 /** 195 * @see Part#getInputStream() 196 */ 197 override 198 InputStream getInputStream() { 199 if (_file !is null) { 200 // written to a file, whether temporary or not 201 return new FileInputStream(_file); 202 } else { 203 // part content is in memory 204 return new ByteArrayInputStream(_bout.getBuffer(), 0, _bout.size()); 205 } 206 } 207 208 /** 209 * @see Part#getSubmittedFileName() 210 */ 211 override 212 string getSubmittedFileName() { 213 return getContentDispositionFilename(); 214 } 215 216 byte[] getBytes() { 217 if (_bout !is null) 218 return _bout.toByteArray(); 219 return null; 220 } 221 222 /** 223 * @see Part#getName() 224 */ 225 override 226 string getName() { 227 return _name; 228 } 229 230 /** 231 * @see Part#getSize() 232 */ 233 override 234 long getSize() { 235 return _size; 236 } 237 238 /** 239 * @see Part#write(string) 240 */ 241 override 242 void write(string fileName) { 243 version(HUNT_DEBUG) infof("writing file: _file=%s, target=%s", _file, fileName); 244 if(fileName.empty) { 245 warning("Target file name can't be empty."); 246 return; 247 } 248 _temporary = false; 249 if (_file.empty) { 250 // part data is only in the ByteArrayOutputStream and never been written to disk 251 _file = buildPath(_tmpDir, fileName); 252 253 FileOutputStream bos = null; 254 try { 255 bos = new FileOutputStream(_file); 256 _bout.writeTo(bos); 257 bos.flush(); 258 } finally { 259 if (bos !is null) 260 bos.close(); 261 _bout = null; 262 } 263 } else { 264 // the part data is already written to a temporary file, just rename it 265 string target = dirName(_file) ~ dirSeparator ~ fileName; 266 version(HUNT_DEBUG) tracef("src: %s, target: %s", _file, target); 267 rename(_file, target); 268 _file = target; 269 } 270 } 271 272 /** 273 * Remove the file, whether or not Part.write() was called on it (ie no longer temporary) 274 * 275 * @see Part#delete() 276 */ 277 override 278 void remove() { 279 if (!_file.empty && _file.exists()) 280 _file.remove(); 281 } 282 283 /** 284 * Only remove tmp files. 285 * 286 * @throws IOException if unable to delete the file 287 */ 288 void cleanUp() { 289 if (_temporary && _file !is null && _file.exists()) 290 _file.remove(); 291 } 292 293 /** 294 * Get the file 295 * 296 * @return the file, if any, the data has been written to. 297 */ 298 string getFile() { 299 return _file; 300 } 301 302 /** 303 * Get the filename from the content-disposition. 304 * 305 * @return null or the filename 306 */ 307 string getContentDispositionFilename() { 308 return _filename; 309 } 310 } 311 312 /** 313 * @param input Request input stream 314 * @param contentType Content-Type header 315 * @param config MultipartConfig 316 * @param contextTmpDir javax.servlet.context.tempdir 317 */ 318 this(InputStream input, string contentType, MultipartConfig config, string contextTmpDir) { 319 _contentType = contentType; 320 _config = config; 321 if (contextTmpDir.empty) 322 _contextTmpDir = tempDir(); 323 else 324 _contextTmpDir = contextTmpDir; 325 326 if (_config is null) 327 _config = new MultipartConfig(_contextTmpDir.asAbsolutePath().array); 328 329 // if (input instanceof ServletInputStream) { 330 // if (((ServletInputStream) input).isFinished()) { 331 // _parts = EMPTY_MAP; 332 // _parsed = true; 333 // return; 334 // } 335 // } 336 _in = new BufferedInputStream(input); 337 } 338 339 /** 340 * @return whether the list of parsed parts is empty 341 */ 342 bool isEmpty() { 343 if (_parts is null) 344 return true; 345 346 List!(Part)[] values = _parts.values(); 347 foreach (List!(Part) partList ; values) { 348 if (partList.size() != 0) 349 return false; 350 } 351 352 return true; 353 } 354 355 /** 356 * Get the already parsed parts. 357 * 358 * @return the parts that were parsed 359 */ 360 // deprecated("") 361 // Collection!(Part) getParsedParts() { 362 // if (_parts is null) 363 // return Collections.emptyList(); 364 365 // Collection<List!(Part)> values = _parts.values(); 366 // List!(Part) parts = new ArrayList<>(); 367 // for (List!(Part) o : values) { 368 // List!(Part) asList = LazyList.getList(o, false); 369 // parts.addAll(asList); 370 // } 371 // return parts; 372 // } 373 374 /** 375 * Delete any tmp storage for parts, and clear out the parts list. 376 */ 377 void deleteParts() { 378 if (!_parsed) 379 return; 380 381 Part[] parts; 382 try { 383 parts = getParts(); 384 } catch (IOException e) { 385 throw new RuntimeException(e); 386 } 387 MultiException err = new MultiException(); 388 389 foreach (Part p ; parts) { 390 try { 391 (cast(MultiPart) p).cleanUp(); 392 } catch (Exception e) { 393 err.add(e); 394 } 395 } 396 _parts.clear(); 397 398 err.ifExceptionThrowRuntime(); 399 } 400 401 /** 402 * Parse, if necessary, the multipart data and return the list of Parts. 403 * 404 * @return the parts 405 * @throws IOException if unable to get the parts 406 */ 407 Part[] getParts() { 408 if (!_parsed) 409 parse(); 410 throwIfError(); 411 412 List!(Part)[] values = _parts.values(); 413 Part[] parts; 414 foreach (List!(Part) o ; values) { 415 foreach(Part p; o) { 416 parts ~= p; 417 } 418 } 419 return parts; 420 } 421 422 /** 423 * Get the named Part. 424 * 425 * @param name the part name 426 * @return the parts 427 * @throws IOException if unable to get the part 428 */ 429 Part getPart(string name) { 430 if (!_parsed) 431 parse(); 432 throwIfError(); 433 return _parts.getValue(name, 0); 434 } 435 436 /** 437 * Throws an exception if one has been latched. 438 * 439 * @throws IOException the exception (if present) 440 */ 441 protected void throwIfError() { 442 if (_err !is null) { 443 _err.next = (new Exception("")); 444 auto ioException = cast(IOException) _err; 445 if (ioException !is null) 446 throw ioException; 447 auto illegalStateException = cast(IllegalStateException) _err; 448 if (illegalStateException !is null) 449 throw illegalStateException; 450 throw new IllegalStateException(_err); 451 } 452 } 453 454 /** 455 * Parse, if necessary, the multipart stream. 456 */ 457 protected void parse() { 458 // have we already parsed the input? 459 if (_parsed) 460 return; 461 _parsed = true; 462 463 try { 464 doParse(); 465 } catch (Exception e) { 466 warningf("Error occurred while parsing: %s", e.msg); 467 _err = e; 468 } 469 } 470 471 private void doParse() { 472 // initialize 473 _parts = new MultiMap!Part(); 474 475 // if its not a multipart request, don't parse it 476 if (_contentType is null || !_contentType.startsWith("multipart/form-data")) 477 return; 478 479 // sort out the location to which to write the files 480 string location = _config.getLocation(); 481 if (location.empty()) 482 _tmpDir = _contextTmpDir; 483 else 484 _tmpDir = buildPath(_contextTmpDir, location); 485 486 if (!_tmpDir.exists()) 487 _tmpDir.mkdirRecurse(); 488 489 string contentTypeBoundary = ""; 490 int bstart = cast(int)_contentType.indexOf("boundary="); 491 if (bstart >= 0) { 492 ptrdiff_t bend = _contentType.indexOf(";", bstart); 493 bend = (bend < 0 ? _contentType.length : bend); 494 contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType[bstart .. bend]).strip()); 495 } 496 497 Handler handler = new Handler(); 498 MultipartParser parser = new MultipartParser(handler, contentTypeBoundary); 499 500 // Create a buffer to store data from stream // 501 byte[] data = new byte[_bufferSize]; 502 int len = 0; 503 504 /* 505 * keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfig._maxRequestSize 506 */ 507 long total = 0; 508 509 while (true) { 510 511 len = _in.read(data); 512 513 if (len > 0) { 514 total += len; 515 if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize()) { 516 _err = new IllegalStateException("Request exceeds maxRequestSize (" ~ 517 _config.getMaxRequestSize().to!string() ~ ")"); 518 return; 519 } 520 521 ByteBuffer buffer = BufferUtils.toBuffer(data); 522 buffer.limit(len); 523 if (parser.parse(buffer, false)) 524 break; 525 526 if (buffer.hasRemaining()) 527 throw new IllegalStateException("Buffer did not fully consume"); 528 529 } else if (len == -1) { 530 parser.parse(BufferUtils.EMPTY_BUFFER, true); 531 break; 532 } 533 534 } 535 536 // check for exceptions 537 if (_err !is null) { 538 return; 539 } 540 541 // check we read to the end of the message 542 if (parser.getState() != MultipartParser.State.END) { 543 if (parser.getState() == MultipartParser.State.PREAMBLE) 544 _err = new IOException("Missing initial multi part boundary"); 545 else 546 _err = new IOException("Incomplete Multipart"); 547 } 548 549 version(HUNT_DEBUG) { 550 tracef("Parsing Complete %s err=%s", parser, _err); 551 } 552 } 553 554 class Handler : MultipartParserHandler { 555 private MultiPart _part = null; 556 private string contentDisposition = null; 557 private string contentType = null; 558 private MultiMap!string headers; 559 560 this() { 561 super(); 562 headers = new MultiMap!string(); 563 } 564 565 override 566 bool messageComplete() { 567 return true; 568 } 569 570 override 571 void parsedField(string key, string value) { 572 // Add to headers and mark if one of these fields. // 573 headers.put(std.uni.toLower(key), value); 574 if (key.equalsIgnoreCase("content-disposition")) 575 contentDisposition = value; 576 else if (key.equalsIgnoreCase("content-type")) 577 contentType = value; 578 579 // Transfer encoding is not longer considers as it is deprecated as per 580 // https://tools.ietf.org/html/rfc7578#section-4.7 581 582 } 583 584 override 585 bool headerComplete() { 586 version(HUNT_DEBUG) { 587 tracef("headerComplete %s", this); 588 } 589 590 try { 591 // Extract content-disposition 592 bool form_data = false; 593 if (contentDisposition is null) { 594 throw new IOException("Missing content-disposition"); 595 } 596 597 QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true); 598 string name = null; 599 string filename = null; 600 while (tok.hasMoreTokens()) { 601 string t = tok.nextToken().strip(); 602 string tl = std.uni.toLower(t); 603 if (tl.startsWith("form-data")) 604 form_data = true; 605 else if (tl.startsWith("name=")) 606 name = value(t); 607 else if (tl.startsWith("filename=")) 608 filename = filenameValue(t); 609 } 610 611 // Check disposition 612 if (!form_data) 613 throw new IOException("Part not form-data"); 614 615 // It is valid for reset and submit buttons to have an empty name. 616 // If no name is supplied, the browser skips sending the info for that field. 617 // However, if you supply the empty string as the name, the browser sends the 618 // field, with name as the empty string. So, only continue this loop if we 619 // have not yet seen a name field. 620 if (name is null) 621 throw new IOException("No name in part"); 622 623 624 // create the new part 625 _part = new MultiPart(name, filename); 626 _part.setHeaders(headers); 627 _part.setContentType(contentType); 628 _parts.add(name, _part); 629 630 try { 631 _part.open(); 632 } catch (IOException e) { 633 _err = e; 634 return true; 635 } 636 } catch (Exception e) { 637 _err = e; 638 return true; 639 } 640 641 return false; 642 } 643 644 override 645 bool content(ByteBuffer buffer, bool last) { 646 if (_part is null) 647 return false; 648 649 if (BufferUtils.hasContent(buffer)) { 650 try { 651 _part.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); 652 } catch (IOException e) { 653 _err = e; 654 return true; 655 } 656 } 657 658 if (last) { 659 try { 660 _part.close(); 661 } catch (IOException e) { 662 _err = e; 663 return true; 664 } 665 } 666 667 return false; 668 } 669 670 override 671 void startPart() { 672 reset(); 673 } 674 675 override 676 void earlyEOF() { 677 version(HUNT_DEBUG) 678 tracef("Early EOF %s", this.outer); 679 } 680 681 void reset() { 682 _part = null; 683 contentDisposition = null; 684 contentType = null; 685 headers = new MultiMap!string(); 686 } 687 } 688 689 void setDeleteOnExit(bool deleteOnExit) { 690 _deleteOnExit = deleteOnExit; 691 } 692 693 void setWriteFilesWithFilenames(bool writeFilesWithFilenames) { 694 _writeFilesWithFilenames = writeFilesWithFilenames; 695 } 696 697 bool isWriteFilesWithFilenames() { 698 return _writeFilesWithFilenames; 699 } 700 701 bool isDeleteOnExit() { 702 return _deleteOnExit; 703 } 704 705 /* ------------------------------------------------------------ */ 706 private string value(string nameEqualsValue) { 707 ptrdiff_t idx = nameEqualsValue.indexOf('='); 708 string value = nameEqualsValue[idx + 1 .. $].strip(); 709 return QuotedStringTokenizer.unquoteOnly(value); 710 } 711 712 /* ------------------------------------------------------------ */ 713 private string filenameValue(string nameEqualsValue) { 714 ptrdiff_t idx = nameEqualsValue.indexOf('='); 715 string value = nameEqualsValue[idx + 1 .. $].strip(); 716 auto pattern = ctRegex!(".??[a-z,A-Z]\\:\\\\[^\\\\].*"); 717 // if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*")) { 718 auto m = matchFirst(value, pattern); 719 if(!m.empty) { 720 // incorrectly escaped IE filenames that have the whole path 721 // we just strip any leading & trailing quotes and leave it as is 722 char first = value[0]; 723 if (first == '"' || first == '\'') 724 value = value[1 .. $]; 725 char last = value[$ - 1]; 726 if (last == '"' || last == '\'') 727 value = value[0 .. $ - 1]; 728 729 return value; 730 } else 731 // unquote the string, but allow any backslashes that don't 732 // form a valid escape sequence to remain as many browsers 733 // even on *nix systems will not escape a filename containing 734 // backslashes 735 return QuotedStringTokenizer.unquoteOnly(value, true); 736 } 737 738 } 739 740 741 /** 742 * <p> This class represents a part or form item that was received within a 743 * <code>multipart/form-data</code> POST request. 744 * 745 * @since Servlet 3.0 746 */ 747 interface Part { 748 749 /** 750 * Gets the content of this part as an <tt>InputStream</tt> 751 * 752 * @return The content of this part as an <tt>InputStream</tt> 753 * @throws IOException If an error occurs in retrieving the contet 754 * as an <tt>InputStream</tt> 755 */ 756 InputStream getInputStream(); 757 758 /** 759 * Gets the content type of this part. 760 * 761 * @return The content type of this part. 762 */ 763 string getContentType(); 764 765 /** 766 * Gets the name of this part 767 * 768 * @return The name of this part as a <tt>string</tt> 769 */ 770 string getName(); 771 772 /** 773 * Gets the file name specified by the client 774 * 775 * @return the submitted file name 776 * 777 * @since Servlet 3.1 778 */ 779 string getSubmittedFileName(); 780 781 /** 782 * Returns the size of this fille. 783 * 784 * @return a <code>long</code> specifying the size of this part, in bytes. 785 */ 786 long getSize(); 787 788 /** 789 * A convenience method to write this uploaded item to disk. 790 * 791 * <p>This method is not guaranteed to succeed if called more than once for 792 * the same part. This allows a particular implementation to use, for 793 * example, file renaming, where possible, rather than copying all of the 794 * underlying data, thus gaining a significant performance benefit. 795 * 796 * @param fileName the name of the file to which the stream will be 797 * written. The file is created relative to the location as 798 * specified in the MultipartConfig 799 * 800 * @throws IOException if an error occurs. 801 */ 802 void write(string fileName); 803 804 /** 805 * Deletes the underlying storage for a file item, including deleting any 806 * associated temporary disk file. 807 * 808 * @throws IOException if an error occurs. 809 */ 810 void remove(); 811 812 /** 813 * 814 * Returns the value of the specified mime header 815 * as a <code>string</code>. If the Part did not include a header 816 * of the specified name, this method returns <code>null</code>. 817 * If there are multiple headers with the same name, this method 818 * returns the first header in the part. 819 * The header name is case insensitive. You can use 820 * this method with any request header. 821 * 822 * @param name a <code>string</code> specifying the 823 * header name 824 * 825 * @return a <code>string</code> containing the 826 * value of the requested 827 * header, or <code>null</code> 828 * if the part does not 829 * have a header of that name 830 */ 831 string getHeader(string name); 832 833 /** 834 * Gets the values of the Part header with the given name. 835 * 836 * <p>Any changes to the returned <code>Collection</code> must not 837 * affect this <code>Part</code>. 838 * 839 * <p>Part header names are case insensitive. 840 * 841 * @param name the header name whose values to return 842 * 843 * @return a (possibly empty) <code>Collection</code> of the values of 844 * the header with the given name 845 */ 846 Collection!string getHeaders(string name); 847 848 /** 849 * Gets the header names of this Part. 850 * 851 * <p>Some servlet containers do not allow 852 * servlets to access headers using this method, in 853 * which case this method returns <code>null</code> 854 * 855 * <p>Any changes to the returned <code>Collection</code> must not 856 * affect this <code>Part</code>. 857 * 858 * @return a (possibly empty) <code>Collection</code> of the header 859 * names of this Part 860 */ 861 string[] getHeaderNames(); 862 863 string toString(); 864 865 }