1 module hunt.http.server.HttpServerRequest;
2 
3 import hunt.http.server.GlobalSettings;
4 import hunt.http.server.Http1ServerConnection;
5 import hunt.http.server.HttpRequestOptions;
6 import hunt.http.server.HttpServerOptions;
7 import hunt.http.server.HttpServerResponse;
8 import hunt.http.server.ServerHttpHandler;
9 
10 import hunt.http.codec.http.decode.HttpParser;
11 import hunt.http.codec.http.decode.MultipartFormParser;
12 import hunt.http.codec.http.model;
13 
14 import hunt.http.Cookie;
15 import hunt.http.HttpFields;
16 import hunt.http.HttpHeader;
17 import hunt.http.HttpMethod;
18 import hunt.http.HttpRequest;
19 import hunt.http.HttpVersion;
20 import hunt.http.MultipartForm;
21 
22 import hunt.collection;
23 import hunt.concurrency.atomic;
24 import hunt.Exceptions;
25 import hunt.Functions;
26 import hunt.io.BufferUtils;
27 import hunt.stream.PipedStream;
28 import hunt.stream.ByteArrayInputStream;
29 import hunt.stream.Common;
30 import hunt.logging;
31 import hunt.net.util.HttpURI;
32 import hunt.net.util.UrlEncoded;
33 import hunt.text.Common;
34 import hunt.util.Common;
35 import hunt.util.MimeTypeUtils;
36 
37 import std.algorithm;
38 import std.array;
39 import std.conv;
40 import std.container.array;
41 import std.json;
42 import std.string : icmp, strip;
43 import std.range;
44 
45 
46 /**
47  * 
48  */
49 class HttpServerRequest : HttpRequest {
50     
51     private string[][string] _formData;  // data buffer for form-data and x-www-form-urlencoded
52     private string[string] _queryParams;
53     
54     private bool _isMultipart = false;
55     private bool _isXFormUrlencoded = false;
56     private bool _isQueryParamsSet = false;
57 
58     private string _stringBody; 
59     private string _mimeType;
60     private HttpServerOptions _options;
61 
62     private Cookie[] _cookies;
63     private PipedStream _pipedStream;
64     private MultipartFormParser _multipartFormParser;
65     private UrlEncoded _urlEncodedMap;
66 
67     private Action1!HttpServerRequest _messageCompleteHandler;
68     private Action1!HttpServerRequest _contentCompleteHandler;  
69 
70     this(string method, string uri, HttpVersion ver, HttpServerOptions options) {
71         enum string connect = HttpMethod.CONNECT.asString();
72         _options = options;
73 
74         super(method, new HttpURI(icmp(method, connect) == 0 ? "http://" ~ uri : uri), 
75             ver, new HttpFields());   
76         // _originalPath = uri;  
77         _originalPath = getURI().getPath();
78         // super(method, new HttpURI(HttpMethod.fromString(method) == HttpMethod.CONNECT ? "http://" ~ uri : uri), ver, new HttpFields());
79     }
80 
81     @property string host() {
82         return header(HttpHeader.HOST);
83     }
84     
85     string getParameter(string name) {
86         decodeUrl();
87         return _urlEncodedMap.getString(name);
88     }
89 
90     List!(string) getParameterValues(string name) {
91         decodeUrl();
92         return _urlEncodedMap.getValues(name);
93     }
94 
95     Map!(string, List!(string)) getParameterMap() {
96         decodeUrl();
97         return _urlEncodedMap;
98     }
99 
100     string getMimeType() {
101         if(_mimeType is null) {
102             _mimeType = MimeTypeUtils.getContentTypeMIMEType(getContentType());
103         }
104 
105         return _mimeType;
106     }
107     
108     private void decodeUrl() {
109         if(_urlEncodedMap !is null)
110             return;
111 
112         _urlEncodedMap = new UrlEncoded();
113 
114         string queryString = getURI().getQuery();
115         if (!queryString.empty()) {
116             _urlEncodedMap.decode(queryString);
117         }
118 
119         // if ("application/x-www-form-urlencoded".equalsIgnoreCase(getMimeType())) {
120         //     string bodyString = getStringBody(_options.requestOptions.getCharset());
121         //     _urlEncodedMap.decode(bodyString);
122         // }
123     }
124 
125     private OutputStream getOutputStream() {
126         return this._pipedStream.getOutputStream();
127     }
128 
129     string getStringBody(string charset) {
130         if (_stringBody !is null) {
131             return _stringBody;
132         } else {
133             if (_pipedStream is null) // no content
134                 return null;
135 
136             InputStream inputStream = this._pipedStream.getInputStream();
137             scope(exit) 
138                 inputStream.close();
139 
140             try  {
141                 // inputStream.position(0); // The InputStream may be read, so reset it before reading it again.
142                 int size = inputStream.available();
143                 version(HUNT_HTTP_DEBUG) tracef("available: %d", size);
144                 // TODO: Tasks pending completion -@zhangxueping at 2019-10-23T15:40:56+08:00
145                 // encoding convertion: to UTF-8
146                 
147                 // skip the read for better performance
148                 byte[] buffer;
149                 ByteArrayInputStream arrayStream = cast(ByteArrayInputStream)inputStream;
150                 if(arrayStream is null) {
151                     buffer = new byte[size];
152                     int number = inputStream.read(buffer);
153                     if(number == -1) {
154                         version(HUNT_DEBUG) warning("no data for read");
155                     }
156                 } else {
157                     buffer = arrayStream.getRawBuffer();
158                 }
159                 _stringBody = cast(string)buffer;
160                 return _stringBody;
161             } catch (IOException e) {
162                 version(HUNT_DEBUG) warning("Get an exception for string body: ", e.msg);
163                 version(HUNT_HTTP_DEBUG) warning(e);
164                 return null;
165             }
166         }
167     }
168 
169     string getStringBody() {
170         return getStringBody("UTF-8");
171     }
172 
173     package(hunt.http) void onHeaderComplete() {
174         // _options = options;
175         auto options = _options.requestOptions;
176         if (isChunked()) {
177             this._pipedStream = new ByteArrayPipedStream(4 * 1024);
178         } else {
179             long contentLength = getContentLength();
180            
181             if (contentLength > options.getBodyBufferThreshold()) {
182                 this._pipedStream = new FilePipedStream(options.getTempFilePath());
183             } else if(contentLength > 0) {
184                 this._pipedStream = new ByteArrayPipedStream(cast(int) contentLength);
185             }
186         }
187     }
188 
189     private shared long chunkedEncodingContentLength;
190 
191     package(hunt.http) void onContent(ByteBuffer buffer) {
192         version(HUNT_HTTP_DEBUG) {
193             tracef("received content size -> %s bytes", buffer.remaining());
194         }
195 
196         try {
197             OutputStream outStream = getOutputStream();
198             if (isChunked()) {
199                 auto options = _options.requestOptions;
200                 long length = AtomicHelper.increment(chunkedEncodingContentLength, buffer.remaining());
201                 ByteArrayPipedStream bytesStream = cast(ByteArrayPipedStream)_pipedStream;
202 
203                 // switch PipedStream from ByteArrayPipedStream to FilePipedStream.
204                 if (length > options.getBodyBufferThreshold() && bytesStream !is null) {
205                     // FIXME: Needing refactor or cleanup -@zhangxueping at 2019-10-17T23:05:10+08:00
206                     // better performance.
207                     
208                     version(HUNT_HTTP_DEBUG) {
209                         warningf("dump to temp file, ContentLength:%d, BufferThreshold: %d, file: %s", 
210                             length,
211                             options.getBodyBufferThreshold(), 
212                             options.getTempFilePath());
213                     }
214 
215                     // chunked encoding content dump to temp file
216                     _pipedStream.getOutputStream().close();
217                     FilePipedStream filePipedStream = new FilePipedStream(options.getTempFilePath());
218                     byte[] bufferedBytes = bytesStream.getOutputStream().toByteArray(false);
219                     OutputStream fileOutStream = filePipedStream.getOutputStream();
220                     fileOutStream.write(bufferedBytes);
221                     fileOutStream.write(BufferUtils.toArray(buffer, false));
222                     _pipedStream = filePipedStream;
223                 } else {
224                     outStream.write(BufferUtils.toArray(buffer, false));
225                 }
226             } else {
227                 outStream.write(BufferUtils.toArray(buffer, false));
228             }
229         } catch (IOException e) {
230             version(HUNT_DEBUG) warning("http server receives http body exception: ", e.msg);
231             version(HUNT_HTTP_DEBUG) warning(e);
232         }        
233     }
234 
235     package(hunt.http) void onContentComplete() {
236         if(_pipedStream is null || (getContentLength() <=0 && !isChunked()) )
237             return;
238             
239         try {
240             string mimeType = getMimeType();
241             version(HUNT_HTTP_DEBUG) trace("mimeType: ", mimeType);
242             getOutputStream().close();
243 
244             if ("multipart/form-data".equalsIgnoreCase(mimeType)) {
245                 _multipartFormParser = new MultipartFormParser(
246                         _pipedStream.getInputStream(),
247                         getContentType(),
248                         GlobalSettings.getMultipartOptions(_options.requestOptions),
249                         _options.requestOptions.getTempFilePath());
250             } else if("application/x-www-form-urlencoded".equalsIgnoreCase(mimeType)) {
251                 _isXFormUrlencoded = true;
252             }
253 
254             if(_contentCompleteHandler !is null) {
255                 _contentCompleteHandler(this);
256             }
257         } catch (IOException e) {
258             version(HUNT_DEBUG) warning("http server ends receiving data exception: ", e.msg);
259             version(HUNT_HTTP_DEBUG) warning(e);
260         }
261     } 
262 
263     package(hunt.http) void onMessageComplete() {
264         if(_messageCompleteHandler !is null)
265             _messageCompleteHandler(this);
266     }
267 
268     // HttpRequest onContent(Action1!ByteBuffer handler) {
269     //     _contentHandler = handler;
270     //     return this;
271     // }
272 
273     HttpRequest onContentComplete(Action1!HttpServerRequest handler) {
274         _contentCompleteHandler = handler;
275         return this;
276     }
277 
278     HttpServerRequest onMessageComplete(Action1!HttpServerRequest handler) {
279         _messageCompleteHandler = handler;
280         return this;
281     }
282 
283     
284     HttpServerRequest onBadMessage(Action1!HttpServerRequest handler) {
285         _messageCompleteHandler = handler;
286         return this;
287     }
288     
289 
290     Cookie[] getCookies() {        
291         if (_cookies is null) {
292 
293             Array!(Cookie) list;
294             foreach (string v; getFields().getValuesList(HttpHeader.COOKIE)) {
295                 if (v.empty)
296                     continue;
297                 foreach (Cookie c; parseCookie(v))
298                     list.insertBack(c);
299             }
300             _cookies = list.array();
301             // _cookies = getFields().getValuesList(HttpHeader.COOKIE)
302 			// 		.map!(parseSetCookie).array;
303         }
304 		
305 		return _cookies;
306     }
307 
308     bool isMultipartForm() {
309         return _multipartFormParser !is null;
310     }
311 
312     bool isXFormUrlencoded() {
313         return _isXFormUrlencoded;
314     }
315 
316     Part[] getParts() {
317         if (_multipartFormParser is null) {
318             throw new RuntimeException("The request is not a multipart form.");
319         }
320 
321         // try {
322             return _multipartFormParser.getParts();
323         // } catch (IOException e) {
324         //     version(HUNT_DEBUG) warning("get multi part exception: ", e.msg);
325         //     version(HUNT_HTTP_DEBUG) warning(e);
326         //     return null;
327         // }
328     }
329 
330     Part getPart(string name) {
331         if (_multipartFormParser is null) {
332             throw new RuntimeException("The reqest is not a multipart form.");
333             // return null;
334         }
335 
336         // try {
337             return _multipartFormParser.getPart(name);
338         // } catch (IOException e) {
339         //     version(HUNT_DEBUG) warning("get multi part exception: ", e.msg);
340         //     version(HUNT_HTTP_DEBUG) warning(e);
341         //     return null;
342         // }
343     } 
344     
345 
346 // dfmtoff Some Helpers
347 
348     import std.string;
349 
350     @property string originalPath() {
351         return _originalPath;
352     }
353     private string _originalPath;
354 
355     @property string path() {
356         return getURI().getPath();
357     }
358 
359     @property void path(string value) {
360         getURI().setPath(value);
361     }
362 
363     @property string decodedPath() {
364         return getURI().getDecodedPath();
365     }
366 
367     string opDispatch(string key)() {
368         version(HUNT_HTTP_DEBUG) {
369             tracef("Get the query parameter: %s", key);
370         }
371         return query(key);
372     }
373 
374     /**
375      * Retrieve a query string item from the request.
376      *
377      * @param  string  key
378      * @param  string|array|null  default
379      * @return string|array
380      */
381     string query(string key, string defaults = null) {
382         return queries().get(key, defaults);
383     }
384 
385     /// get a query
386     T get(T = string)(string key, T v = T.init) {
387         version(HUNT_HTTP_DEBUG) {
388             tracef("key: %s, value: %s", key, v);
389         }
390 
391         auto tmp = queries();
392         if (tmp is null) {
393             return v;
394         }
395         
396         static if(is(T == string)) {
397             if(v is null) v = "";
398         }
399 
400         auto _v = tmp.get(key, "");
401         version(HUNT_HTTP_DEBUG) {
402             tracef("key: %s, value: %s, target: %s, default: %s", key, _v, T.stringof, v);
403         }
404 
405         if (_v.length) {
406             try {
407                 return to!T(_v);
408             } catch(Exception ex) {
409                 string msg = format("Failed converting from <%s> to %s", _v, T.stringof);
410                 warning(msg);
411                 // warning(ex);
412                 //throw new Exception(msg);
413                 return v;
414             }
415         }
416         
417         return v;
418     }
419 
420     /**
421      * Retrieve a request payload item from the request.
422      *
423      * @param  string  key
424      * @param  string|array|null  default
425      *
426      * @return string|array
427      */
428     T post(T = string)(string key, T v = T.init) {
429 
430         string[][string] form = formData();
431         
432         if(key in form) {
433             string[] _v = form[key];
434             if (_v.length > 0) {
435                 static if(is(T == string))
436                     v = _v[0];
437                 else {
438                     v = to!T(_v[0]);
439                 }
440             } 
441         }                 
442             
443         return v;
444     }
445 
446     T[] posts(T = string)(string key, T[] v = null) {
447         string[][string] form = formData();
448         if (form is null)
449             return v;
450             
451         if(key in form) {
452             string[] _v = form[key];
453             if (_v.length > 0) {
454                 static if(is(T == string))
455                     v = _v[];
456                 else {
457                     v = new T[_v.length];
458                     for(size i =0; i<v.length; i++) {
459                         v[i] = to!T(_v[i]);
460                     }
461                 }
462             } 
463         } 
464 
465         return v;
466     }
467 
468     // get queries
469     @property ref string[string] queries() {
470         if (!_isQueryParamsSet) {
471             MultiMap!string map = getURI().decodeQuery();
472             foreach (string key, List!(string) values; map) {
473                 version(HUNT_DEBUG) {
474                     infof("query parameter: key=%s, values=%s", key, values[0]);
475                 }
476                 if(values is null || values.size()<1) {
477                     _queryParams[key] = ""; 
478                 } else {
479                     _queryParams[key] = values[0];
480                 }
481             }
482             _isQueryParamsSet = true;
483         }
484         return _queryParams;
485     }
486 
487     void putQueryParameter(string key, string value) {
488         version(HUNT_HTTP_DEBUG) infof("query parameter: key=%s, values=%s", key, value);
489         _queryParams[key] = value;
490     }
491 
492     /**
493      * Replace the input for the current request.
494      *
495      * @param  array input
496      * @return Request
497      */
498     void replace(string[string] input) {
499         if (isContained(this.getMethod(), ["GET", "HEAD"]))
500             _queryParams = input;
501         else {
502             foreach(string k, string v; input) {
503                 _formData[k] ~= v;
504             }
505         }
506     }
507 
508     private static bool isContained(string source, string[] keys) {
509         foreach (string k; keys) {
510             if (canFind(source, k))
511                 return true;
512         }
513 
514         return false;
515     }
516     
517     T bindForm(T)() if(is(T == class) || is(T == struct)) {
518         if(getMethod() != HttpMethod.POST.asString() && 
519             getMethod() != HttpMethod.PUT.asString()) {
520             version(HUNT_DEBUG) {
521                 warningf("The required method is POST or PUT. Here is yours: %s", getMethod());
522             }
523             return T.init;
524         }
525 
526         import hunt.serialization.JsonSerializer;
527 
528         JSONValue jv;
529         string[][string] forms = formData();
530         if(forms is null) {
531             static if(is(T == class)) {
532                 return new T();
533             } else {
534                 return T.init;
535             }
536         }
537 
538         foreach(string k, string[] values; forms) {
539             if(values.length > 1) {
540                 jv[k] = JSONValue(values);
541             } else if(values.length == 1) {
542                 jv[k] = JSONValue(values[0]);
543             } else {
544                 warningf("null value for %s in form data: ", k);
545             }
546         }
547         
548         import hunt.serialization.Common;
549         return JsonSerializer.toObject!(T, SerializationOptions.Default.canThrow(false))(jv);
550     }
551 
552     deprecated("Using formData instead.")
553     alias xFormData = formData;
554 
555     @property string[][string] formData() {
556         if (_formData !is null) 
557             return _formData;
558 
559         if(_isXFormUrlencoded) {
560             UrlEncoded map = new UrlEncoded(UrlEncodeStyle.HtmlForm);
561             map.decode(getStringBody());
562             foreach (string key; map.byKey()) {
563                 foreach(string v; map.getValues(key)) {
564                     key = key.strip();
565                     _formData[key] ~= v.strip();
566                 }
567             }
568         } else if (isMultipartForm()) {
569             foreach (Part part; getParts()) {
570                 MultipartForm multipart = cast(MultipartForm) part;
571 
572                 string contentType = multipart.getContentType();
573                 string submittedFileName = multipart.getSubmittedFileName();
574                 string key = multipart.getName();
575                 if (submittedFileName.empty) {
576                     string content = cast(string)multipart.getBytes();
577                     content = content.strip();
578                     version (HUNT_HTTP_DEBUG) {
579                         if(content.length > 128) {
580                             content = content[0..128] ~ "...";
581                         } 
582 
583                         tracef("File: key=%s, fileName=%s, actualFile=%s, ContentType=%s, content=%s",
584                                 key, submittedFileName, multipart.getFile(), multipart.getContentType(),
585                                 content); 
586                     } 
587 
588                     _formData[key] ~= content;
589                 } 
590             }            
591         }
592 
593         return _formData;
594     }
595 
596     /**
597      * Retrieve a cookie from the request.
598      *
599      * @param  string  key
600      * @param  string|array|null  default
601      * @return string|array
602      */
603     string cookie(string key, string defaultValue = null) {
604         // return cookieManager.get(key, defaultValue);
605         foreach (Cookie c; getCookies()) {
606             if (c.getName == key)
607                 return c.getValue();
608         }
609 
610         return defaultValue;
611     }
612 
613     /**
614      * Retrieve  users' own preferred language.
615      */
616     string locale() {
617         string l;
618         l = cookie("Content-Language");
619         if(l is null)
620             l = _options.requestOptions.defaultLanguage();
621 
622         return toLower(l);
623     }
624     
625 
626 // dfmton
627 
628 
629 }