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 }