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; 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 = DEFAULT_GURAD_NAME; 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 string content = cast(string)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 }