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 }