1 /*
2  * Hunt - A high-level D Programming Language Web framework that encourages rapid development and clean, pragmatic design.
3  *
4  * Copyright (C) 2015-2019, HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.framework.http.Request;
13 
14 import hunt.framework.auth;
15 import hunt.framework.file.UploadedFile;
16 import hunt.framework.http.session.SessionStorage;
17 import hunt.framework.Init;
18 import hunt.framework.provider.ServiceProvider;
19 import hunt.framework.routing;
20 
21 import hunt.http.AuthenticationScheme;
22 import hunt.http.Cookie;
23 import hunt.http.HttpMethod;
24 import hunt.http.HttpHeader;
25 import hunt.http.MultipartForm;
26 import hunt.http.server.HttpServerRequest;
27 import hunt.http.server.HttpSession;
28 import hunt.logging.ConsoleLogger;
29 import hunt.serialization.JsonSerializer;
30 
31 import std.algorithm;
32 import std.array : split;
33 import std.base64;
34 import std.json;
35 import std.format;
36 import std.range;
37 import std.socket;
38 
39 import core.time;
40 
41 
42 enum BasicTokenHeader = AuthenticationScheme.Basic ~ " ";
43 enum BearerTokenHeader = AuthenticationScheme.Bearer ~ " ";
44 
45 /**
46  * 
47  */
48 class Request {
49 
50     private HttpSession _session;
51     private SessionStorage _sessionStorage;
52     private bool _isMultipart = false;
53     private bool _isXFormUrlencoded = false;
54     private UploadedFile[] _convertedAllFiles;
55     private UploadedFile[][string] _convertedMultiFiles;
56     private string _routeGroup = DEFAULT_ROUTE_GROUP;
57     private string _actionId = "";
58     private Auth _auth;
59     private string _guardName;
60     private MonoTime _monoCreated;
61     private bool _isRestful = false;
62 
63     HttpServerRequest _request;
64     alias _request this;
65 
66     this(HttpServerRequest request, Address remoteAddress, RouterContex routeContext=null) {
67         _request = request;
68         if(routeContext !is null) {
69             ActionRouteItem routeItem = cast(ActionRouteItem)routeContext.routeItem;
70             if(routeItem !is null)
71                 _actionId = routeItem.actionId;
72             _routeGroup = routeContext.routeGroup.name;
73             _guardName = routeContext.routeGroup.guardName;
74         }
75         _monoCreated = MonoTime.currTime;
76         _sessionStorage = serviceContainer().resolve!SessionStorage();
77         _remoteAddr = remoteAddress;
78 
79         .request(this); // Binding this request to the current thread.
80     }
81 
82     Auth auth() {
83         if(_auth is null) {
84             _auth = new Auth(this);
85         }
86         return _auth;
87     }
88 
89     bool isRestful() {
90         return _isRestful;
91     }
92 
93     void isRestful(bool value) {
94         _isRestful = value;
95     }
96     
97 
98     /**
99      * Determine if the uploaded data contains a file.
100      *
101      * @param  string  key
102      * @return bool
103      */
104     bool hasFile(string key) {
105         if (!isMultipartForm()) {
106             return false;
107         } else {
108             checkUploadedFiles();
109 
110             if (_convertedMultiFiles is null || _convertedMultiFiles.get(key, null) is null) {
111                 return false;
112             }
113             return true;
114         }
115     }
116 
117     private void checkUploadedFiles() {
118         if (!_convertedAllFiles.empty()) 
119             return;
120 
121         foreach (Part part; _request.getParts()) {
122             MultipartForm multipart = cast(MultipartForm) part;
123 
124             version (HUNT_HTTP_DEBUG) {
125                 cast(string) content = multipart.getBytes();
126                 if(content.length > 128) {
127                     content = content[0..128] ~ "...";
128                 }
129 
130                 tracef("File: key=%s, fileName=%s, actualFile=%s, ContentType=%s, content=%s",
131                         multipart.getName(), multipart.getSubmittedFileName(),
132                         multipart.getFile(), multipart.getContentType(),
133                         content); 
134             }
135 
136             string contentType = multipart.getContentType();
137             string submittedFileName = multipart.getSubmittedFileName();
138             string key = multipart.getName();
139             if (!submittedFileName.empty) {
140                 // TODO: for upload failed? What's the errorCode? use multipart.isWriteToFile?
141                 int errorCode = 0;
142                 multipart.flush();
143                 auto file = new UploadedFile(multipart.getFile(),
144                         submittedFileName, contentType, errorCode);
145 
146                 this._convertedMultiFiles[key] ~= file;
147                 this._convertedAllFiles ~= file;
148             }
149         }
150     }
151 
152 
153     /**
154      * Retrieve a file from the request.
155      *
156      * @param  string  key
157      * @param  mixed default
158      * @return UploadedFile
159      */
160     UploadedFile file(string key)
161     {
162         if (this.hasFile(key))
163         {
164             return this._convertedMultiFiles[key][0];
165         }
166 
167         return null;
168     }
169 
170     UploadedFile[] files(string key)
171     {
172         if (this.hasFile(key))
173         {
174             return this._convertedMultiFiles[key];
175         }
176 
177         return null;
178     }
179 
180     @property int elapsed()    {
181         Duration timeElapsed = MonoTime.currTime - _monoCreated;
182         return cast(int)timeElapsed.total!"msecs";
183     }
184 
185 //     /**
186 //      * Custom parameters.
187 //      */
188 //     @property string[string] mate() {
189 //         return _mate;
190 //     }
191 
192 //     string getMate(string key, string value = null) {
193 //         return _mate.get(key, value);
194 //     }
195 
196 //     long size() @property
197 //     {
198 //         return _stringBody.length;
199 //     }
200 
201 //     void addMate(string key, string value) {
202 //         _mate[key] = value;
203 //     }
204 
205 //     @property string host() {
206 //         return header(HttpHeader.HOST);
207 //     }
208 
209 //     string header(HttpHeader code) {
210 //         return getFields().get(code);
211 //     }
212 
213 //     string header(string key) {
214 //         return getFields().get(key);
215 //     }
216 
217     bool headerExists(HttpHeader code) {
218         return getFields().contains(code);
219     }
220 
221     bool headerExists(string key) {
222         return getFields().containsKey(key);
223     }
224 
225 //     // int headersForeach(scope int delegate(string key, string value) each)
226 //     // {
227 //     //     return getFields().opApply(each);
228 //     // }
229 
230 //     // int headersForeach(scope int delegate(HttpHeader code, string key, string value) each)
231 //     // {
232 //     //     return getFields().opApply(each);
233 //     // }
234 
235 //     // bool headerValueForeach(string name, scope bool delegate(string value) func)
236 //     // {
237 //     //     return getFields().forEachValueOfHeader(name, func);
238 //     // }
239 
240 //     // bool headerValueForeach(HttpHeader code, scope bool delegate(string value) func)
241 //     // {
242 //     //     return getFields().forEachValueOfHeader(code, func);
243 //     // }
244 
245 //     @property string referer() {
246 //         string rf = header("Referer");
247 //         string[] rfarr = split(rf, ", ");
248 //         if (rfarr.length) {
249 //             return rfarr[0];
250 //         }
251 //         return "";
252 //     }
253 
254     @property Address remoteAddr() {
255         return _remoteAddr;
256     }
257     private Address _remoteAddr;
258 
259     @property string ip() {
260         string s = this.header(HttpHeader.X_FORWARDED_FOR);
261         if(s.empty) {
262             s = this.header("Proxy-Client-IP");
263         } else {
264             auto arr = s.split(",");
265             if(arr.length >= 0)
266                 s = arr[0];
267         }
268 
269         if(s.empty) {
270             s = this.header("WL-Proxy-Client-IP");
271         }
272 
273         if(s.empty) {
274             s = this.header("HTTP_CLIENT_IP");
275         }
276 
277         if(s.empty) {
278             s = this.header("HTTP_X_FORWARDED_FOR");
279         } 
280 
281         if(s.empty) {
282             Address ad = remoteAddr();
283             s = ad.toAddrString();
284         }
285 
286         return s;
287     }    
288 
289     @property JSONValue json() {
290         if (_json == JSONValue.init)
291             _json = parseJSON(getBodyAsString());
292         return _json;
293     }
294     private JSONValue _json;
295 
296 
297     string getBodyAsString() {
298         if (stringBody is null) {
299             stringBody = _request.getStringBody();
300         }
301         return stringBody;
302     }
303     private string stringBody;
304 
305     private static bool isContained(string source, string[] keys) {
306         foreach (string k; keys) {
307             if (canFind(source, k))
308                 return true;
309         }
310         return false;
311     }
312 
313     string actionId() {
314         return _actionId;
315     }
316 
317     string routeGroup() {
318         return _routeGroup;
319     }
320 
321     string guardName() {
322         return _guardName;
323     }
324 
325     /**
326      * Flush all of the old input from the session.
327      *
328      * @return void
329      */
330     void flush() {
331         if (_session !is null)
332             _sessionStorage.put(_session);
333     }
334 
335     /**
336      * Gets the HttpSession.
337      *
338      * @return HttpSession|null The session
339      */
340     @property HttpSession session(bool canCreate = true) {
341         if (_session !is null || isSessionRetrieved)
342             return _session;
343 
344         string sessionId = this.cookie(DefaultSessionIdName);
345         isSessionRetrieved = true;
346         if (!sessionId.empty) {
347             _session = _sessionStorage.get(sessionId);
348             if(_session !is null) {
349                 _session.setMaxInactiveInterval(_sessionStorage.expire);
350                 version(HUNT_HTTP_DEBUG) {
351                     tracef("session exists: %s, expire: %d", sessionId, _session.getMaxInactiveInterval());
352                 }
353             }
354         }
355 
356         if (_session is null && canCreate) {
357             sessionId = HttpSession.generateSessionId();
358             version(HUNT_DEBUG) infof("new session: %s, expire: %d", sessionId, _sessionStorage.expire);
359             _session = HttpSession.create(sessionId, _sessionStorage.expire);
360         }
361 
362         return _session;
363     }
364 
365     private bool isSessionRetrieved = false;
366 
367     /**
368      * Whether the request contains a HttpSession object.
369      *
370      * This method does not give any information about the state of the session object,
371      * like whether the session is started or not. It is just a way to check if this Request
372      * is associated with a HttpSession instance.
373      *
374      * @return bool true when the Request contains a HttpSession object, false otherwise
375      */
376     bool hasSession() {
377         return session() !is null;
378     }
379 
380     /**
381      * Get the bearer token from the request headers.
382      *
383      * @return string
384      */
385     string bearerToken() {
386         string v = _request.header("Authorization");
387         if (startsWith(v, BearerTokenHeader)) {
388             return v[BearerTokenHeader.length .. $];
389         }
390         return null;
391     }
392 
393     /**
394      * Get the basic token from the request headers.
395      *
396      * @return string
397      */
398     string basicToken() {
399         string v = _request.header("Authorization");
400         if (startsWith(v, BasicTokenHeader)) {
401             return v[BasicTokenHeader.length .. $];
402         }
403         return null;
404     }
405 
406     /**
407      * Determine if the request contains a given input item key.
408      *
409      * @param  string|array key
410      * @return bool
411      */
412     bool exists(string key) {
413         return has([key]);
414     }
415 
416     /**
417      * Determine if the request contains a given input item key.
418      *
419      * @param  string|array  key
420      * @return bool
421      */
422     bool has(string[] keys) {
423         string[string] dict = this.all();
424         foreach (string k; keys) {
425             string* p = (k in dict);
426             if (p is null)
427                 return false;
428         }
429         return true;
430     }
431 
432     /**
433      * Determine if the request contains any of the given inputs.
434      *
435      * @param  dynamic  key
436      * @return bool
437      */
438     bool hasAny(string[] keys...) {
439         string[string] dict = this.all();
440         foreach (string k; keys) {
441             string* p = (k in dict);
442             if (p is null)
443                 return true;
444         }
445         return false;
446     }
447 
448     /**
449      * Determine if the request contains a non-empty value for an input item.
450      *
451      * @param  string|array  key
452      * @return bool
453      */
454     bool filled(string[] keys) {
455         foreach (string k; keys) {
456             if (k.empty)
457                 return false;
458         }
459 
460         return true;
461     }
462 
463 //     /**
464 //      * Get the keys for all of the input and files.
465 //      *
466 //      * @return array
467 //      */
468 //     string[] keys() {
469 //         // return this.input().keys ~ this.httpForm.fileKeys();
470 //         implementationMissing(false);
471 //         return this.input().keys;
472 //     }
473 
474     /**
475      * Get all of the input and files for the request.
476      *
477      * @param  array|mixed  keys
478      * @return array
479      */
480     string[string] all(string[] keys = null) {
481         string[string] inputs = this.input();
482         if (keys is null) {
483             // HttpForm.FormFile[string]  files = this.allFiles;
484             // foreach(string k; files.byKey)
485             // {
486             //     inputs[k] = files[k].fileName;
487             // }
488             return inputs;
489         }
490 
491         string[string] results;
492         foreach (string k; keys) {
493             string* v = (k in inputs);
494             if (v !is null)
495                 results[k] = *v;
496         }
497         return results;
498     }
499 
500     /**
501      * Retrieve an input item from the request.
502      *
503      * @param  string  key
504      * @param  string|array|null  default
505      * @return string|array
506      */
507     string input(string key, string defaults = null) {
508         return getInputSource().get(key, defaults);
509     }
510 
511     /// ditto
512     string[string] input() {
513         return getInputSource();
514     }
515 
516     /**
517      * Retrieve a cookie from the request.
518      *
519      * @param  string  key
520      * @param  string|array|null  default
521      * @return string|array
522      */
523     string cookie(string key, string defaultValue = null) {
524         foreach (Cookie c; getCookies()) {
525             if (c.getName == key)
526                 return c.getValue();
527         }
528         return defaultValue;
529     }
530 
531     /**
532      * Get an array of all of the files on the request.
533      *
534      * @return array
535      */
536     UploadedFile[] allFiles() {
537         checkUploadedFiles();
538         return _convertedAllFiles;
539     }
540 
541 
542     @property string methodAsString() {
543         return _request.getMethod();
544     }
545 
546     @property HttpMethod method() {
547         return HttpMethod.fromString(_request.getMethod());
548     }
549 
550     @property string url() {
551         return _request.getURIString();
552     }
553 
554     @property string fullUrl()
555     {
556         string str = format("%s://%s%s", getScheme(), _request.host(), _request.getURI().toString());
557         return str;
558     }
559 
560     @property string path() {
561         return _request.getURI().getPath();
562     }
563 
564 //     @property string decodedPath() {
565 //         return _request.getURI().getDecodedPath();
566 //     }
567 
568     /**
569      * Gets the request's scheme.
570      *
571      * @return string
572      */
573     string getScheme() {
574         return _request.isHttps() ? "https" : "http";
575     }
576 
577 
578     protected string[string] getInputSource() {
579         if (isContained(this.methodAsString, ["GET", "HEAD"]))
580             return queries();
581         else {
582             string[string] r;
583             foreach(string k, string[] v; xFormData()) {
584                 r[k] = v[0];
585             }
586             return r;
587         }
588     }
589 
590 }
591 
592 
593 // version(WITH_HUNT_TRACE) {
594 //     import hunt.trace.Tracer;
595 // }
596 
597 
598 private Request _request;
599 
600 Request request() {
601     return _request;
602 }
603 
604 void request(Request request) {
605     _request = request;
606 }
607 
608 HttpSession session() {
609     return request().session();
610 }