1 module requests.http; 2 3 private: 4 import std.algorithm; 5 import std.array; 6 import std.ascii; 7 import std.conv; 8 import std.datetime; 9 import std.exception; 10 import std.format; 11 import std.stdio; 12 import std.range; 13 import std.string; 14 import std.traits; 15 import std.typecons; 16 import std.experimental.logger; 17 import core.thread; 18 19 import requests.streams; 20 import requests.uri; 21 import requests.utils; 22 import requests.base; 23 import requests.connmanager; 24 import requests.rangeadapter; 25 26 static immutable ushort[] redirectCodes = [301, 302, 303, 307, 308]; 27 28 enum HTTP11 = 101; 29 enum HTTP10 = 100; 30 31 static immutable string[string] proxies; 32 shared static this() { 33 import std.process; 34 import std.string; 35 foreach(p; ["http", "https", "all"]) 36 { 37 auto v = environment.get(p ~ "_proxy", environment.get(p.toUpper() ~ "_PROXY")); 38 if ( v !is null && v.length > 0 ) 39 { 40 debug(requests) tracef("will use %s for %s as proxy", v, p); 41 proxies[p] = v; 42 } 43 } 44 } 45 46 public class MaxRedirectsException: Exception { 47 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow { 48 super(message, file, line, next); 49 } 50 } 51 52 /// 53 /// 54 /// 55 //public auto queryParams(T...)(T params) pure nothrow @safe 56 //{ 57 // static assert (T.length % 2 == 0, "wrong args count"); 58 // 59 // QueryParam[] output; 60 // output.reserve = T.length / 2; 61 // 62 // void queryParamsHelper(T...)(T params, ref QueryParam[] output) 63 // { 64 // static if (T.length > 0) 65 // { 66 // output ~= QueryParam(params[0].to!string, params[1].to!string); 67 // queryParamsHelper(params[2..$], output); 68 // } 69 // } 70 // 71 // queryParamsHelper(params, output); 72 // return output; 73 //} 74 75 /// 76 /// Response - result of request execution. 77 /// 78 /// Response.code - response HTTP code. 79 /// Response.status_line - received HTTP status line. 80 /// Response.responseHeaders - received headers. 81 /// Response.responseBody - container for received body 82 /// Response.history - for redirected responses contain all history 83 /// 84 public class HTTPResponse : Response { 85 private { 86 string _status_line; 87 88 HTTPResponse[] _history; // redirects history 89 90 mixin(Setter!string("status_line")); 91 92 int _version; 93 } 94 95 ~this() { 96 _responseHeaders = null; 97 _history.length = 0; 98 } 99 100 mixin(Getter("status_line")); 101 102 @property final string[string] responseHeaders() @safe @nogc nothrow { 103 return _responseHeaders; 104 } 105 @property final HTTPResponse[] history() @safe @nogc nothrow { 106 return _history; 107 } 108 109 private int parse_version(in string v) pure const nothrow @safe { 110 // try to parse HTTP/1.x to version 111 try if ( v.length > 5 ) { 112 return (v[5..$].split(".").map!"to!int(a)".array[0..2].reduce!((a,b) => a*100 + b)); 113 } catch (Exception e) { 114 } 115 return 0; 116 } 117 unittest { 118 auto r = new HTTPResponse(); 119 assert(r.parse_version("HTTP/1.1") == 101); 120 assert(r.parse_version("HTTP/1.0") == 100); 121 assert(r.parse_version("HTTP/0.9") == 9); 122 assert(r.parse_version("HTTP/xxx") == 0); 123 } 124 } 125 126 /// 127 /// Request. 128 /// Configurable parameters: 129 /// $(B method) - string, method to use (GET, POST, ...) 130 /// $(B headers) - string[string], add any additional headers you'd like to send. 131 /// $(B authenticator) - class Auth, class to send auth headers. 132 /// $(B keepAlive) - bool, set true for keepAlive requests. default true. 133 /// $(B maxRedirects) - uint, maximum number of redirects. default 10. 134 /// $(B maxHeadersLength) - size_t, maximum length of server response headers. default = 32KB. 135 /// $(B maxContentLength) - size_t, maximun content length. delault - 0 = unlimited. 136 /// $(B bufferSize) - size_t, send and receive buffer size. default = 16KB. 137 /// $(B verbosity) - uint, level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0. 138 /// $(B proxy) - string, set proxy url if needed. default - null. 139 /// $(B cookie) - Tuple Cookie, Read/Write cookie You can get cookie setted by server, or set cookies before doing request. 140 /// $(B timeout) - Duration, Set timeout value for connect/receive/send. 141 /// 142 public struct HTTPRequest { 143 private { 144 string _method = "GET"; 145 URI _uri; 146 string[string] _headers; 147 string[] _filteredHeaders; 148 Auth _authenticator; 149 bool _keepAlive = true; 150 uint _maxRedirects = 10; 151 size_t _maxHeadersLength = 32 * 1024; // 32 KB 152 size_t _maxContentLength; // 0 - Unlimited 153 string _proxy; 154 uint _verbosity = 0; // 0 - no output, 1 - headers, 2 - headers+body info 155 Duration _timeout = 30.seconds; 156 size_t _bufferSize = 16*1024; // 16k 157 bool _useStreaming; // return iterator instead of completed request 158 159 HTTPResponse[] _history; // redirects history 160 DataPipe!ubyte _bodyDecoder; 161 DecodeChunked _unChunker; 162 long _contentLength; 163 long _contentReceived; 164 SSLOptions _sslOptions; 165 string _bind; 166 _UH _userHeaders; 167 168 RefCounted!ConnManager _cm; 169 RefCounted!Cookies _cookie; 170 string[URI] _permanent_redirects; // cache 301 redirects for GET requests 171 MultipartForm _multipartForm; 172 173 NetStreamFactory _socketFactory; 174 175 QueryParam[] _params; 176 string _contentType; 177 InputRangeAdapter _postData; 178 } 179 package HTTPResponse _response; 180 181 mixin(Getter_Setter!string ("method")); 182 mixin(Getter_Setter!bool ("keepAlive")); 183 mixin(Getter_Setter!size_t ("maxContentLength")); 184 mixin(Getter_Setter!size_t ("maxHeadersLength")); 185 mixin(Getter_Setter!size_t ("bufferSize")); 186 mixin(Getter_Setter!uint ("maxRedirects")); 187 mixin(Getter_Setter!uint ("verbosity")); 188 mixin(Getter ("proxy")); 189 mixin(Getter_Setter!Duration ("timeout")); 190 mixin(Setter!Auth ("authenticator")); 191 mixin(Getter_Setter!bool ("useStreaming")); 192 mixin(Getter ("contentLength")); 193 mixin(Getter ("contentReceived")); 194 mixin(Getter_Setter!SSLOptions ("sslOptions")); 195 mixin(Getter_Setter!string ("bind")); 196 mixin(Setter!NetStreamFactory ("socketFactory")); 197 198 @property void sslSetVerifyPeer(bool v) pure @safe nothrow @nogc { 199 _sslOptions.setVerifyPeer(v); 200 } 201 @property void sslSetKeyFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc { 202 _sslOptions.setKeyFile(p, t); 203 } 204 @property void sslSetCertFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc { 205 _sslOptions.setCertFile(p, t); 206 } 207 @property void sslSetCaCert(string path) pure @safe nothrow @nogc { 208 _sslOptions.setCaCert(path); 209 } 210 //@property final void cookie(Cookie[] s) pure @safe @nogc nothrow { 211 // _cookie = s; 212 //} 213 @property final void proxy(string v) { 214 if ( v != _proxy ) { 215 _cm.clear(); 216 } 217 _proxy = v; 218 } 219 //@property final Cookie[] cookie() pure @safe @nogc nothrow { 220 // return _cookie; 221 //} 222 223 this(string uri) { 224 _uri = URI(uri); 225 _cm = ConnManager(10); 226 } 227 ~this() { 228 _headers = null; 229 _authenticator = null; 230 _history = null; 231 _bodyDecoder = null; 232 _unChunker = null; 233 //if ( _cm ) { 234 // _cm.clear(); 235 //} 236 } 237 string toString() const { 238 return "HTTPRequest(%s, %s)".format(_method, _uri.uri()); 239 } 240 string format(string fmt) const { 241 import std.array; 242 import std.stdio; 243 auto a = appender!string(); 244 auto f = FormatSpec!char(fmt); 245 while (f.writeUpToNextSpec(a)) { 246 switch(f.spec) { 247 case 'h': 248 // Remote hostname. 249 a.put(_uri.host); 250 break; 251 case 'm': 252 // method. 253 a.put(_method); 254 break; 255 case 'p': 256 // Remote port. 257 a.put("%d".format(_uri.port)); 258 break; 259 case 'P': 260 // Path 261 a.put(_uri.path); 262 break; 263 case 'q': 264 // query parameters supplied with url. 265 a.put(_uri.query); 266 break; 267 case 'U': 268 a.put(_uri.uri()); 269 break; 270 default: 271 throw new FormatException("Unknown Request format spec " ~ f.spec); 272 } 273 } 274 return a.data(); 275 } 276 string select_proxy(string scheme) { 277 if ( _proxy is null && proxies.length == 0 ) { 278 debug(requests) tracef("proxy=null"); 279 return null; 280 } 281 if ( _proxy ) { 282 debug(requests) tracef("proxy=%s", _proxy); 283 return _proxy; 284 } 285 auto p = scheme in proxies; 286 if ( p !is null && *p != "") { 287 debug(requests) tracef("proxy=%s", *p); 288 return *p; 289 } 290 p = "all" in proxies; 291 if ( p !is null && *p != "") { 292 debug(requests) tracef("proxy=%s", *p); 293 return *p; 294 } 295 debug(requests) tracef("proxy=null"); 296 return null; 297 } 298 void clearHeaders() { 299 _headers = null; 300 } 301 @property void uri(in URI newURI) { 302 //handleURLChange(_uri, newURI); 303 _uri = newURI; 304 } 305 /// Add headers to request 306 /// Params: 307 /// headers = headers to send. 308 void addHeaders(in string[string] headers) { 309 foreach(pair; headers.byKeyValue) { 310 string _h = pair.key; 311 switch(toLower(_h)) { 312 case "host": 313 _userHeaders.Host = true; 314 break; 315 case "user-agent": 316 _userHeaders.UserAgent = true; 317 break; 318 case "content-length": 319 _userHeaders.ContentLength = true; 320 break; 321 case "content-type": 322 _userHeaders.ContentType = true; 323 break; 324 case "connection": 325 _userHeaders.Connection = true; 326 break; 327 case "cookie": 328 _userHeaders.Cookie = true; 329 break; 330 default: 331 break; 332 } 333 _headers[pair.key] = pair.value; 334 } 335 } 336 private void safeSetHeader(ref string[string] headers, bool userAdded, string h, string v) pure @safe { 337 if ( !userAdded ) { 338 headers[h] = v; 339 } 340 } 341 /// Remove headers from request 342 /// Params: 343 /// headers = headers to remove. 344 void removeHeaders(in string[] headers) pure { 345 _filteredHeaders ~= headers; 346 } 347 /// 348 /// compose headers to send 349 /// 350 private string[string] requestHeaders() { 351 352 string[string] generatedHeaders; 353 354 if ( _authenticator ) { 355 _authenticator. 356 authHeaders(_uri.host). 357 byKeyValue. 358 each!(pair => generatedHeaders[pair.key] = pair.value); 359 } 360 361 _headers.byKey.each!(h => generatedHeaders[h] = _headers[h]); 362 363 safeSetHeader(generatedHeaders, _userHeaders.AcceptEncoding, "Accept-Encoding", "gzip,deflate"); 364 safeSetHeader(generatedHeaders, _userHeaders.UserAgent, "User-Agent", "dlang-requests"); 365 safeSetHeader(generatedHeaders, _userHeaders.Connection, "Connection", _keepAlive?"Keep-Alive":"Close"); 366 367 if ( !_userHeaders.Host ) 368 { 369 generatedHeaders["Host"] = _uri.host; 370 if ( _uri.scheme !in standard_ports || _uri.port != standard_ports[_uri.scheme] ) { 371 generatedHeaders["Host"] ~= ":%d".format(_uri.port); 372 } 373 } 374 375 if ( _cookie._array.length && !_userHeaders.Cookie ) { 376 auto cs = _cookie._array. 377 filter!(c => _uri.path.pathMatches(c.path) && _uri.host.domainMatches(c.domain)). 378 map!(c => "%s=%s".format(c.attr, c.value)). 379 joiner(";"); 380 if ( ! cs.empty ) 381 { 382 generatedHeaders["Cookie"] = to!string(cs); 383 } 384 } 385 386 _filteredHeaders.each!(h => generatedHeaders.remove(h)); 387 388 return generatedHeaders; 389 } 390 /// 391 /// Build request string. 392 /// Handle proxy and query parameters. 393 /// 394 private @property string requestString(QueryParam[] params = null) { 395 auto query = _uri.query.dup; 396 if ( params ) { 397 query ~= "&" ~ params2query(params); 398 if ( query[0] != '?' ) { 399 query = "?" ~ query; 400 } 401 } 402 string actual_proxy = select_proxy(_uri.scheme); 403 if ( actual_proxy && _uri.scheme != "https" ) { 404 return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.uri(No.params), query); 405 } 406 return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.path, query); 407 } 408 /// 409 /// encode parameters and build query part of the url 410 /// 411 private static string params2query(in QueryParam[] params) pure @safe { 412 return params. 413 map!(a => "%s=%s".format(a.key.urlEncoded, a.value.urlEncoded)). 414 join("&"); 415 } 416 // 417 package unittest { 418 assert(params2query(queryParams("a","b", "c", " d "))=="a=b&c=%20d%20"); 419 } 420 /// 421 /// Analyze received headers, take appropriate actions: 422 /// check content length, attach unchunk and uncompress 423 /// 424 private void analyzeHeaders(in string[string] headers) { 425 426 _contentLength = -1; 427 _unChunker = null; 428 auto contentLength = "content-length" in headers; 429 if ( contentLength ) { 430 try { 431 string l = *contentLength; 432 _contentLength = parse!long(l); 433 // TODO: maybe add a strict mode that checks if l was parsed completely 434 if ( _maxContentLength && _contentLength > _maxContentLength) { 435 throw new RequestException("ContentLength > maxContentLength (%d>%d)". 436 format(_contentLength, _maxContentLength)); 437 } 438 } catch (ConvException e) { 439 throw new RequestException("Can't convert Content-Length from %s".format(*contentLength)); 440 } 441 } 442 auto transferEncoding = "transfer-encoding" in headers; 443 if ( transferEncoding ) { 444 debug(requests) tracef("transferEncoding: %s", *transferEncoding); 445 if ( (*transferEncoding).toLower == "chunked") { 446 _unChunker = new DecodeChunked(); 447 _bodyDecoder.insert(_unChunker); 448 } 449 } 450 auto contentEncoding = "content-encoding" in headers; 451 if ( contentEncoding ) switch (*contentEncoding) { 452 default: 453 throw new RequestException("Unknown content-encoding " ~ *contentEncoding); 454 case "gzip": 455 case "deflate": 456 _bodyDecoder.insert(new Decompressor!ubyte); 457 } 458 459 } 460 /// 461 /// Called when we know that all headers already received in buffer. 462 /// This routine does not interpret headers content (see analyzeHeaders). 463 /// 1. Split headers on lines 464 /// 2. store status line, store response code 465 /// 3. unfold headers if needed 466 /// 4. store headers 467 /// 468 private void parseResponseHeaders(in ubyte[] input, string lineSep) { 469 string lastHeader; 470 auto buffer = cast(string)input; 471 472 foreach(line; buffer.split(lineSep).map!(l => l.stripRight)) { 473 if ( ! _response.status_line.length ) { 474 debug (requests) tracef("statusLine: %s", line); 475 _response.status_line = line; 476 if ( _verbosity >= 1 ) { 477 writefln("< %s", line); 478 } 479 auto parsed = line.split(" "); 480 if ( parsed.length >= 2 ) { 481 _response.code = parsed[1].to!ushort; 482 _response._version = _response.parse_version(parsed[0]); 483 } 484 continue; 485 } 486 if ( line[0] == ' ' || line[0] == '\t' ) { 487 // unfolding https://tools.ietf.org/html/rfc822#section-3.1 488 if ( auto stored = lastHeader in _response._responseHeaders) { 489 *stored ~= line; 490 } 491 continue; 492 } 493 auto parsed = line.findSplit(":"); 494 auto header = parsed[0].toLower; 495 auto value = parsed[2].strip; 496 497 if ( _verbosity >= 1 ) { 498 writefln("< %s: %s", header, value); 499 } 500 501 lastHeader = header; 502 debug (requests) tracef("Header %s = %s", header, value); 503 504 if ( header != "set-cookie" ) { 505 auto stored = _response.responseHeaders.get(header, null); 506 if ( stored ) { 507 value = stored ~ "," ~ value; 508 } 509 _response._responseHeaders[header] = value; 510 continue; 511 } 512 _cookie._array ~= processCookie(value); 513 } 514 } 515 516 /// 517 /// Process Set-Cookie header from server response 518 /// 519 private Cookie[] processCookie(string value ) pure { 520 // cookie processing 521 // 522 // as we can't join several set-cookie lines in single line 523 // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com 524 // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com, cs=ip764-RgKqc-HvSkxRxdQQAKW8LA; path=/; domain=.example.com; HttpOnly 525 // 526 Cookie[] res; 527 string[string] kv; 528 auto fields = value.split(";").map!strip; 529 while(!fields.empty) { 530 auto s = fields.front.findSplit("="); 531 fields.popFront; 532 if ( s[1] != "=" ) { 533 continue; 534 } 535 auto k = s[0]; 536 auto v = s[2]; 537 switch(k.toLower()) { 538 case "domain": 539 k = "domain"; 540 break; 541 case "path": 542 k = "path"; 543 break; 544 case "expires": 545 continue; 546 case "max-age": 547 continue; 548 default: 549 break; 550 } 551 kv[k] = v; 552 } 553 if ( "domain" !in kv ) { 554 kv["domain"] = _uri.host; 555 } 556 if ( "path" !in kv ) { 557 kv["path"] = _uri.path; 558 } 559 auto domain = kv["domain"]; kv.remove("domain"); 560 auto path = kv["path"]; kv.remove("path"); 561 foreach(pair; kv.byKeyValue) { 562 auto _attr = pair.key; 563 auto _value = pair.value; 564 auto cookie = Cookie(path, domain, _attr, _value); 565 res ~= cookie; 566 } 567 return res; 568 } 569 570 private bool willFollowRedirect() { 571 if ( !canFind(redirectCodes, _response.code) ) { 572 return false; 573 } 574 if ( !_maxRedirects ) { 575 return false; 576 } 577 if ( "location" !in _response.responseHeaders ) { 578 return false; 579 } 580 return true; 581 } 582 private URI uriFromLocation(const ref URI uri, in string location) { 583 URI newURI = uri; 584 try { 585 newURI = URI(location); 586 } catch (UriException e) { 587 debug(requests) trace("Can't parse Location:, try relative uri"); 588 newURI.path = location; 589 newURI.uri = newURI.recalc_uri; 590 } 591 return newURI; 592 } 593 /// 594 /// if we have new uri, then we need to check if we have to reopen existent connection 595 /// 596 private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) { 597 if (url is null && _uri.uri == "" ) { 598 throw new RequestException("No url configured", file, line); 599 } 600 601 if ( url !is null ) { 602 URI newURI = URI(url); 603 //handleURLChange(_uri, newURI); 604 _uri = newURI; 605 } 606 } 607 /// 608 /// Setup connection. Handle proxy and https case 609 /// 610 /// Place new connection in ConnManager cache 611 /// 612 private NetworkStream setupConnection() 613 do { 614 615 debug(requests) tracef("Set up new connection"); 616 NetworkStream stream; 617 618 // on exit 619 // place created connection to conn. manager 620 // close connection purged from manager (if any) 621 // 622 scope(exit) { 623 if ( stream ) 624 { 625 if ( auto purged_connection = _cm.put(_uri.scheme, _uri.host, _uri.port, stream) ) 626 { 627 debug(requests) tracef("closing purged connection %s", purged_connection); 628 purged_connection.close(); 629 } 630 } 631 } 632 633 if ( _socketFactory ) 634 { 635 debug(requests) tracef("use socketFactory"); 636 stream = _socketFactory(_uri.scheme, _uri.host, _uri.port); 637 } 638 639 if ( stream ) // socket factory created connection 640 { 641 return stream; 642 } 643 644 URI uri; // this URI will be used temporarry if we need proxy 645 string actual_proxy = select_proxy(_uri.scheme); 646 final switch (_uri.scheme) { 647 case"http": 648 if ( actual_proxy ) { 649 uri.uri_parse(actual_proxy); 650 uri.idn_encode(); 651 } else { 652 // use original uri 653 uri = _uri; 654 } 655 stream = new TCPStream(); 656 stream.bind(_bind); 657 stream.connect(uri.host, uri.port, _timeout); 658 break ; 659 case"https": 660 if ( actual_proxy ) { 661 uri.uri_parse(actual_proxy); 662 uri.idn_encode(); 663 stream = new TCPStream(); 664 stream.bind(_bind); 665 stream.connect(uri.host, uri.port, _timeout); 666 if ( verbosity>=1 ) { 667 writeln("> CONNECT %s:%d HTTP/1.1".format(_uri.host, _uri.port)); 668 } 669 stream.send("CONNECT %s:%d HTTP/1.1\r\n\r\n".format(_uri.host, _uri.port)); 670 while ( stream.isConnected ) { 671 ubyte[1024] b; 672 auto read = stream.receive(b); 673 if ( verbosity>=1) { 674 writefln("< %s", cast(string)b[0..read]); 675 } 676 debug(requests) tracef("read: %d", read); 677 if ( b[0..read].canFind("\r\n\r\n") || b[0..read].canFind("\n\n") ) { 678 debug(requests) tracef("proxy connection ready"); 679 // convert connection to ssl 680 stream = new SSLStream(stream, _sslOptions, _uri.host); 681 break ; 682 } else { 683 debug(requests) tracef("still wait for proxy connection"); 684 } 685 } 686 } else { 687 uri = _uri; 688 stream = new SSLStream(_sslOptions); 689 stream.bind(_bind); 690 stream.connect(uri.host, uri.port, _timeout); 691 debug(requests) tracef("ssl connection to origin server ready"); 692 } 693 break ; 694 } 695 696 return stream; 697 } 698 /// 699 /// Request sent, now receive response. 700 /// Find headers, split on headers and body, continue to receive body 701 /// 702 private void receiveResponse(NetworkStream _stream) { 703 704 try { 705 _stream.readTimeout = timeout; 706 } catch (Exception e) { 707 debug(requests) tracef("Failed to set read timeout for stream: %s", e.msg); 708 return; 709 } 710 // Commented this out as at exit we can have alreade closed socket 711 // scope(exit) { 712 // if ( _stream && _stream.isOpen ) { 713 // _stream.readTimeout = 0.seconds; 714 // } 715 // } 716 717 _bodyDecoder = new DataPipe!ubyte(); 718 scope(exit) { 719 if ( !_useStreaming ) { 720 _bodyDecoder = null; 721 _unChunker = null; 722 } 723 } 724 725 auto buffer = Buffer!ubyte(); 726 Buffer!ubyte partialBody; 727 ptrdiff_t read; 728 string lineSep = null, headersEnd = null; 729 bool headersHaveBeenReceived; 730 731 while( !headersHaveBeenReceived ) { 732 733 auto b = new ubyte[_bufferSize]; 734 read = _stream.receive(b); 735 736 debug(requests) tracef("read: %d", read); 737 if ( read == 0 ) { 738 break; 739 } 740 auto data = b[0..read]; 741 buffer.putNoCopy(data); 742 if ( verbosity>=3 ) { 743 writeln(data.dump.join("\n")); 744 } 745 746 if ( buffer.length > maxHeadersLength ) { 747 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength)); 748 } 749 750 // Proper HTTP uses "\r\n" as a line separator, but broken servers sometimes use "\n". 751 // Servers that use "\r\n" might have "\n" inside a header. 752 // For any half-sane server, the first '\n' should be at the end of the status line, so this can be used to detect the line separator. 753 // In any case, all the interesting points in the header for now are at '\n' characters, so scan the newly read data for them. 754 foreach (idx; buffer.length-read..buffer.length) 755 { 756 if ( buffer[idx] == '\n' ) 757 { 758 if ( lineSep is null ) 759 { 760 // First '\n'. Detect line/header endings. 761 // HTTP header sections end with a double line separator 762 lineSep = "\n"; 763 headersEnd = "\n\n"; 764 if ( idx > 0 && buffer[idx-1] == '\r' ) 765 { 766 lineSep = "\r\n"; 767 headersEnd = "\r\n\r\n"; 768 } 769 } 770 else 771 { 772 // Potential header ending. 773 if ( buffer.data[0..idx+1].endsWith(headersEnd) ) 774 { 775 auto ResponseHeaders = buffer.data[0..idx+1-headersEnd.length]; 776 partialBody = buffer[idx+1..$]; 777 _contentReceived += partialBody.length; 778 parseResponseHeaders(ResponseHeaders, lineSep); 779 headersHaveBeenReceived = true; 780 break; 781 } 782 } 783 } 784 } 785 } 786 787 analyzeHeaders(_response._responseHeaders); 788 789 _bodyDecoder.putNoCopy(partialBody.data); 790 791 auto v = _bodyDecoder.get(); 792 _response._responseBody.putNoCopy(v); 793 794 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4 795 if ( (_method == "HEAD") || responseMustNotIncludeBody(_response.code) || (_contentLength < 0 && _unChunker is null) ) 796 { 797 debug(requests) tracef("response without body"); 798 return; 799 } 800 801 _response._contentLength = _contentLength; 802 _response._contentReceived = _contentReceived; 803 804 if ( _verbosity >= 2 ) writefln("< %d bytes of body received", partialBody.length); 805 806 while( true ) { 807 if ( _contentLength >= 0 && _contentReceived >= _contentLength ) { 808 debug(requests) trace("Body received."); 809 break; 810 } 811 if ( _unChunker && _unChunker.done ) { 812 break; 813 } 814 815 if ( _useStreaming && _response._responseBody.length && !redirectCodes.canFind(_response.code) ) { 816 debug(requests) trace("streaming requested"); 817 // save _stream in closure 818 auto __stream = _stream; 819 auto __bodyDecoder = _bodyDecoder; 820 auto __unChunker = _unChunker; 821 auto __contentReceived = _contentReceived; 822 auto __contentLength = _contentLength; 823 auto __bufferSize = _bufferSize; 824 auto __response = _response; 825 auto __verbosity = _verbosity; 826 827 // set up response 828 _response._contentLength = _contentLength; 829 _response.receiveAsRange.activated = true; 830 _response.receiveAsRange.data = _response._responseBody.data; 831 _response.receiveAsRange.read = delegate ubyte[] () { 832 833 while(true) { 834 // check if we received everything we need 835 if ( ( __unChunker && __unChunker.done ) 836 || !__stream.isConnected() 837 || (__contentLength > 0 && __contentReceived >= __contentLength) ) 838 { 839 debug(requests) trace("streaming_in receive completed"); 840 __bodyDecoder.flush(); 841 return __bodyDecoder.get(); 842 } 843 // have to continue 844 auto b = new ubyte[__bufferSize]; 845 try { 846 read = __stream.receive(b); 847 } 848 catch (Exception e) { 849 throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e); 850 } 851 debug(requests) tracef("streaming_in received %d bytes", read); 852 853 if ( read == 0 ) { 854 debug(requests) tracef("streaming_in: server closed connection"); 855 __bodyDecoder.flush(); 856 return __bodyDecoder.get(); 857 } 858 859 if ( __verbosity>=3 ) { 860 writeln(b[0..read].dump.join("\n")); 861 } 862 __response._contentReceived += read; 863 __contentReceived += read; 864 __bodyDecoder.putNoCopy(b[0..read]); 865 auto res = __bodyDecoder.getNoCopy(); 866 if ( res.length == 0 ) { 867 // there were nothing to produce (beginning of the chunk or no decompressed data) 868 continue; 869 } 870 if (res.length == 1) { 871 return res[0]; 872 } 873 // 874 // I'd like to "return _bodyDecoder.getNoCopy().join;" but it is slower 875 // 876 auto total = res.map!(b=>b.length).sum; 877 // create buffer for joined bytes 878 ubyte[] joined = new ubyte[total]; 879 size_t p; 880 // memcopy 881 foreach(ref _; res) { 882 joined[p .. p + _.length] = _; 883 p += _.length; 884 } 885 return joined; 886 } 887 assert(0); 888 }; 889 // we prepared for streaming 890 return; 891 } 892 893 auto b = new ubyte[_bufferSize]; 894 read = _stream.receive(b); 895 896 if ( read == 0 ) { 897 debug(requests) trace("read done"); 898 break; 899 } 900 if ( _verbosity >= 2 ) { 901 writefln("< %d bytes of body received", read); 902 } 903 904 if ( verbosity>=3 ) { 905 writeln(b[0..read].dump.join("\n")); 906 } 907 908 debug(requests) tracef("read: %d", read); 909 _contentReceived += read; 910 if ( _maxContentLength && _contentReceived > _maxContentLength ) { 911 throw new RequestException("ContentLength > maxContentLength (%d>%d)". 912 format(_contentLength, _maxContentLength)); 913 } 914 915 _bodyDecoder.putNoCopy(b[0..read]); // send buffer to all decoders 916 917 _bodyDecoder.getNoCopy. // fetch result and place to body 918 each!(b => _response._responseBody.putNoCopy(b)); 919 920 debug(requests) tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", _contentReceived, _contentLength, _response._responseBody.length); 921 922 } 923 _bodyDecoder.flush(); 924 _response._responseBody.putNoCopy(_bodyDecoder.get()); 925 _response._contentReceived = _contentReceived; 926 } 927 /// 928 /// Check that we received anything. 929 /// Server can close previous connection (keepalive or not) 930 /// 931 private bool serverPrematurelyClosedConnection() pure @safe { 932 immutable server_closed_connection = _response._responseHeaders.length == 0 && _response._status_line.length == 0; 933 // debug(requests) tracef("server closed connection = %s (headers.length=%s, status_line.length=%s)", 934 // server_closed_connection, _response._responseHeaders.length, _response._status_line.length); 935 return server_closed_connection; 936 } 937 private bool isIdempotent(in string method) pure @safe nothrow { 938 return ["GET", "HEAD"].canFind(method); 939 } 940 /// 941 /// If we do not want keepalive request, 942 /// or server signalled to close connection, 943 /// then close it 944 /// 945 void close_connection_if_not_keepalive(NetworkStream _stream) { 946 auto connection = "connection" in _response._responseHeaders; 947 if ( !_keepAlive ) { 948 _stream.close(); 949 } else switch(_response._version) { 950 case HTTP11: 951 // HTTP/1.1 defines the "close" connection option for the sender to signal that the connection 952 // will be closed after completion of the response. For example, 953 // Connection: close 954 // in either the request or the response header fields indicates that the connection 955 // SHOULD NOT be considered `persistent' (section 8.1) after the current request/response is complete. 956 // HTTP/1.1 applications that do not support persistent connections MUST include the "close" connection 957 // option in every message. 958 if ( connection && (*connection).toLower.split(",").canFind("close") ) { 959 _stream.close(); 960 } 961 break; 962 default: 963 // for anything else close connection if there is no keep-alive in Connection 964 if ( connection && !(*connection).toLower.split(",").canFind("keep-alive") ) { 965 _stream.close(); 966 } 967 break; 968 } 969 } 970 /// 971 /// Send multipart for request. 972 /// You would like to use this method for sending large portions of mixed data or uploading files to forms. 973 /// Content of the posted form consist of sources. Each source have at least name and value (can be string-like object or opened file, see more docs for MultipartForm struct) 974 /// Params: 975 /// url = url 976 /// sources = array of sources. 977 deprecated("Use Request() instead of HTTPRequest(); will be removed 2019-07") 978 HTTPResponse exec(string method="POST")(string url, MultipartForm sources) { 979 import std.uuid; 980 import std.file; 981 982 checkURL(url); 983 //if ( _cm is null ) { 984 // _cm = new ConnManager(); 985 //} 986 987 NetworkStream _stream; 988 _method = method; 989 _response = new HTTPResponse; 990 _response.uri = _uri; 991 _response.finalURI = _uri; 992 bool restartedRequest = false; 993 994 connect: 995 _contentReceived = 0; 996 _response._startedAt = Clock.currTime; 997 998 assert(_stream is null); 999 1000 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1001 1002 if ( _stream is null ) { 1003 debug(requests) trace("create new connection"); 1004 _stream = setupConnection(); 1005 } else { 1006 debug(requests) trace("reuse old connection"); 1007 } 1008 1009 assert(_stream !is null); 1010 1011 if ( !_stream.isConnected ) { 1012 debug(requests) trace("disconnected stream on enter"); 1013 if ( !restartedRequest ) { 1014 debug(requests) trace("disconnected stream on enter: retry"); 1015 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1016 1017 _cm.del(_uri.scheme, _uri.host, _uri.port); 1018 _stream.close(); 1019 _stream = null; 1020 1021 restartedRequest = true; 1022 goto connect; 1023 } 1024 debug(requests) trace("disconnected stream on enter: return response"); 1025 //_stream = null; 1026 return _response; 1027 } 1028 _response._connectedAt = Clock.currTime; 1029 1030 Appender!string req; 1031 req.put(requestString()); 1032 1033 string boundary = randomUUID().toString; 1034 string[] partHeaders; 1035 size_t contentLength; 1036 1037 foreach(ref part; sources._sources) { 1038 string h = "--" ~ boundary ~ "\r\n"; 1039 string disposition = `form-data; name="%s"`.format(part.name); 1040 string optionals = part. 1041 parameters.byKeyValue(). 1042 filter!(p => p.key!="Content-Type"). 1043 map! (p => "%s=%s".format(p.key, p.value)). 1044 join("; "); 1045 1046 h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n"; 1047 1048 auto contentType = "Content-Type" in part.parameters; 1049 if ( contentType ) { 1050 h ~= "Content-Type: " ~ *contentType ~ "\r\n"; 1051 } 1052 1053 h ~= "\r\n"; 1054 partHeaders ~= h; 1055 contentLength += h.length + part.input.getSize() + "\r\n".length; 1056 } 1057 contentLength += "--".length + boundary.length + "--\r\n".length; 1058 1059 auto h = requestHeaders(); 1060 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "multipart/form-data; boundary=" ~ boundary); 1061 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(contentLength)); 1062 1063 h.byKeyValue. 1064 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1065 each!(h => req.put(h)); 1066 req.put("\r\n"); 1067 1068 debug(requests) trace(req.data); 1069 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 1070 1071 try { 1072 _stream.send(req.data()); 1073 foreach(ref source; sources._sources) { 1074 debug(requests) tracef("sending part headers <%s>", partHeaders.front); 1075 _stream.send(partHeaders.front); 1076 partHeaders.popFront; 1077 while (true) { 1078 auto chunk = source.input.read(); 1079 if ( chunk.length <= 0 ) { 1080 break; 1081 } 1082 _stream.send(chunk); 1083 } 1084 _stream.send("\r\n"); 1085 } 1086 _stream.send("--" ~ boundary ~ "--\r\n"); 1087 _response._requestSentAt = Clock.currTime; 1088 receiveResponse(_stream); 1089 _response._finishedAt = Clock.currTime; 1090 } 1091 catch (NetworkException e) { 1092 errorf("Error sending request: ", e.msg); 1093 _stream.close(); 1094 return _response; 1095 } 1096 1097 if ( serverPrematurelyClosedConnection() 1098 && !restartedRequest 1099 && isIdempotent(_method) 1100 ) { 1101 /// 1102 /// We didn't receive any data (keepalive connectioin closed?) 1103 /// and we can restart this request. 1104 /// Go ahead. 1105 /// 1106 debug(requests) tracef("Server closed keepalive connection"); 1107 1108 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1109 1110 _cm.del(_uri.scheme, _uri.host, _uri.port); 1111 _stream.close(); 1112 _stream = null; 1113 1114 restartedRequest = true; 1115 goto connect; 1116 } 1117 1118 if ( _useStreaming ) { 1119 if ( _response._receiveAsRange.activated ) { 1120 debug(requests) trace("streaming_in activated"); 1121 return _response; 1122 } else { 1123 // this can happen if whole response body received together with headers 1124 _response._receiveAsRange.data = _response.responseBody.data; 1125 } 1126 } 1127 1128 close_connection_if_not_keepalive(_stream); 1129 1130 if ( _verbosity >= 1 ) { 1131 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1132 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1133 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1134 } 1135 1136 if ( willFollowRedirect ) { 1137 if ( _history.length >= _maxRedirects ) { 1138 _stream = null; 1139 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1140 } 1141 // "location" in response already checked in canFollowRedirect 1142 immutable new_location = *("location" in _response.responseHeaders); 1143 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1144 1145 // save current response for history 1146 _history ~= _response; 1147 1148 // prepare new response (for redirected request) 1149 _response = new HTTPResponse; 1150 _response.uri = current_uri; 1151 _response.finalURI = next_uri; 1152 _stream = null; 1153 1154 // set new uri 1155 this._uri = next_uri; 1156 debug(requests) tracef("Redirected to %s", next_uri); 1157 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1158 // 307 and 308 do not change method 1159 return this.get(); 1160 } 1161 if ( restartedRequest ) { 1162 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1163 restartedRequest = false; 1164 } 1165 goto connect; 1166 } 1167 1168 _response._history = _history; 1169 return _response; 1170 } 1171 1172 // we use this if we send from ubyte[][] and user provided Content-Length 1173 private void sendFlattenContent(T)(NetworkStream _stream, T content) { 1174 while ( !content.empty ) { 1175 auto chunk = content.front; 1176 _stream.send(chunk); 1177 content.popFront; 1178 } 1179 debug(requests) tracef("sent"); 1180 } 1181 // we use this if we send from ubyte[][] as chunked content 1182 private void sendChunkedContent(T)(NetworkStream _stream, T content) { 1183 while ( !content.empty ) { 1184 auto chunk = content.front; 1185 auto chunkHeader = "%x\r\n".format(chunk.length); 1186 debug(requests) tracef("sending %s%s", chunkHeader, chunk); 1187 _stream.send(chunkHeader); 1188 _stream.send(chunk); 1189 _stream.send("\r\n"); 1190 content.popFront; 1191 } 1192 debug(requests) tracef("sent"); 1193 _stream.send("0\r\n\r\n"); 1194 } 1195 /// 1196 /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked). 1197 /// When rank 1 (flat array) used as content it must have length. In that case "content" will be sent directly to network, and Content-Length headers will be added. 1198 /// If you are goung to send some range and do not know length at the moment when you start to send request, then you can send chunks of chars or ubyte. 1199 /// Try not to send too short chunks as this will put additional load on client and server. Chunks of length 2048 or 4096 are ok. 1200 /// 1201 /// Parameters: 1202 /// url = url 1203 /// content = string or input range 1204 /// contentType = content type 1205 /// Returns: 1206 /// Response 1207 /// Examples: 1208 /// --------------------------------------------------------------------------------------------------------- 1209 /// rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 1210 /// 1211 /// auto s = lineSplitter("one,\ntwo,\nthree."); 1212 /// rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 1213 /// 1214 /// auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1215 /// rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 1216 /// 1217 /// auto f = File("tests/test.txt", "rb"); 1218 /// rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 1219 /// -------------------------------------------------------------------------------------------------------- 1220 deprecated("Use Request() instead of HTTPRequest(); will be removed 2019-07") 1221 HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="application/octet-stream") 1222 if ( (rank!R == 1) 1223 || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 1224 || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte))) 1225 ) 1226 do { 1227 debug(requests) tracef("started url=%s, this._uri=%s", url, _uri); 1228 1229 checkURL(url); 1230 //if ( _cm is null ) { 1231 // _cm = new ConnManager(); 1232 //} 1233 1234 NetworkStream _stream; 1235 _method = method; 1236 _response = new HTTPResponse; 1237 _history.length = 0; 1238 _response.uri = _uri; 1239 _response.finalURI = _uri; 1240 bool restartedRequest = false; 1241 bool send_flat; 1242 1243 connect: 1244 _contentReceived = 0; 1245 _response._startedAt = Clock.currTime; 1246 1247 assert(_stream is null); 1248 1249 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1250 1251 if ( _stream is null ) { 1252 debug(requests) trace("create new connection"); 1253 _stream = setupConnection(); 1254 } else { 1255 debug(requests) trace("reuse old connection"); 1256 } 1257 1258 assert(_stream !is null); 1259 1260 if ( !_stream.isConnected ) { 1261 debug(requests) trace("disconnected stream on enter"); 1262 if ( !restartedRequest ) { 1263 debug(requests) trace("disconnected stream on enter: retry"); 1264 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1265 1266 _cm.del(_uri.scheme, _uri.host, _uri.port); 1267 _stream.close(); 1268 _stream = null; 1269 1270 restartedRequest = true; 1271 goto connect; 1272 } 1273 debug(requests) trace("disconnected stream on enter: return response"); 1274 //_stream = null; 1275 return _response; 1276 } 1277 _response._connectedAt = Clock.currTime; 1278 1279 Appender!string req; 1280 req.put(requestString()); 1281 1282 auto h = requestHeaders; 1283 if ( contentType ) { 1284 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", contentType); 1285 } 1286 static if ( rank!R == 1 ) { 1287 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(content.length)); 1288 } else { 1289 if ( _userHeaders.ContentLength ) { 1290 debug(requests) tracef("User provided content-length for chunked content"); 1291 send_flat = true; 1292 } else { 1293 h["Transfer-Encoding"] = "chunked"; 1294 send_flat = false; 1295 } 1296 } 1297 h.byKeyValue. 1298 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1299 each!(h => req.put(h)); 1300 req.put("\r\n"); 1301 1302 debug(requests) trace(req.data); 1303 if ( _verbosity >= 1 ) { 1304 req.data.splitLines.each!(a => writeln("> " ~ a)); 1305 } 1306 1307 try { 1308 // send headers 1309 _stream.send(req.data()); 1310 // send body 1311 static if ( rank!R == 1) { 1312 _stream.send(content); 1313 } else { 1314 if ( send_flat ) { 1315 sendFlattenContent(_stream, content); 1316 } else { 1317 sendChunkedContent(_stream, content); 1318 } 1319 } 1320 _response._requestSentAt = Clock.currTime; 1321 debug(requests) trace("starting receive response"); 1322 receiveResponse(_stream); 1323 debug(requests) trace("finished receive response"); 1324 _response._finishedAt = Clock.currTime; 1325 } catch (NetworkException e) { 1326 _stream.close(); 1327 throw new RequestException("Network error during data exchange"); 1328 } 1329 1330 if ( serverPrematurelyClosedConnection() 1331 && !restartedRequest 1332 && isIdempotent(_method)) 1333 { 1334 /// 1335 /// We didn't receive any data (keepalive connectioin closed?) 1336 /// and we can restart this request. 1337 /// Go ahead. 1338 /// 1339 debug(requests) tracef("Server closed keepalive connection"); 1340 1341 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1342 1343 _cm.del(_uri.scheme, _uri.host, _uri.port); 1344 _stream.close(); 1345 _stream = null; 1346 1347 restartedRequest = true; 1348 goto connect; 1349 } 1350 1351 if ( _useStreaming ) { 1352 if ( _response._receiveAsRange.activated ) { 1353 debug(requests) trace("streaming_in activated"); 1354 return _response; 1355 } else { 1356 // this can happen if whole response body received together with headers 1357 _response._receiveAsRange.data = _response.responseBody.data; 1358 } 1359 } 1360 1361 close_connection_if_not_keepalive(_stream); 1362 1363 if ( _verbosity >= 1 ) { 1364 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1365 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1366 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1367 } 1368 1369 1370 if ( willFollowRedirect ) { 1371 if ( _history.length >= _maxRedirects ) { 1372 _stream = null; 1373 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1374 } 1375 // "location" in response already checked in canFollowRedirect 1376 immutable new_location = *("location" in _response.responseHeaders); 1377 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1378 1379 // save current response for history 1380 _history ~= _response; 1381 1382 // prepare new response (for redirected request) 1383 _response = new HTTPResponse; 1384 _response.uri = current_uri; 1385 _response.finalURI = next_uri; 1386 1387 _stream = null; 1388 1389 // set new uri 1390 this._uri = next_uri; 1391 debug(requests) tracef("Redirected to %s", next_uri); 1392 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1393 // 307 and 308 do not change method 1394 return this.get(); 1395 } 1396 if ( restartedRequest ) { 1397 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1398 restartedRequest = false; 1399 } 1400 goto connect; 1401 } 1402 1403 _response._history = _history; 1404 return _response; 1405 } 1406 /// 1407 /// Send request with parameters. 1408 /// If used for POST or PUT requests then application/x-www-form-urlencoded used. 1409 /// Request parameters will be encoded into request string or placed in request body for POST/PUT 1410 /// requests. 1411 /// Parameters: 1412 /// url = url 1413 /// params = request parameters 1414 /// Returns: 1415 /// Response 1416 /// Examples: 1417 /// --------------------------------------------------------------------------------- 1418 /// rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]); 1419 /// --------------------------------------------------------------------------------- 1420 /// 1421 deprecated("Use Request() instead of HTTPRequest; will be removed 2019-07") 1422 HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null) 1423 do { 1424 debug(requests) tracef("started url=%s, this._uri=%s", url, _uri); 1425 1426 checkURL(url); 1427 //if ( _cm is null ) { 1428 // _cm = new ConnManager(); 1429 //} 1430 1431 NetworkStream _stream; 1432 _method = method; 1433 _response = new HTTPResponse; 1434 _history.length = 0; 1435 _response.uri = _uri; 1436 _response.finalURI = _uri; 1437 bool restartedRequest = false; // True if this is restarted keepAlive request 1438 1439 connect: 1440 if ( _method == "GET" && _uri in _permanent_redirects ) { 1441 debug(requests) trace("use parmanent redirects cache"); 1442 _uri = uriFromLocation(_uri, _permanent_redirects[_uri]); 1443 _response._finalURI = _uri; 1444 } 1445 _contentReceived = 0; 1446 _response._startedAt = Clock.currTime; 1447 1448 assert(_stream is null); 1449 1450 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1451 1452 if ( _stream is null ) { 1453 debug(requests) trace("create new connection"); 1454 _stream = setupConnection(); 1455 } else { 1456 debug(requests) trace("reuse old connection"); 1457 } 1458 1459 assert(_stream !is null); 1460 1461 if ( !_stream.isConnected ) { 1462 debug(requests) trace("disconnected stream on enter"); 1463 if ( !restartedRequest ) { 1464 debug(requests) trace("disconnected stream on enter: retry"); 1465 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1466 1467 _cm.del(_uri.scheme, _uri.host, _uri.port); 1468 _stream.close(); 1469 _stream = null; 1470 1471 restartedRequest = true; 1472 goto connect; 1473 } 1474 debug(requests) trace("disconnected stream on enter: return response"); 1475 //_stream = null; 1476 return _response; 1477 } 1478 _response._connectedAt = Clock.currTime; 1479 1480 auto h = requestHeaders(); 1481 1482 Appender!string req; 1483 1484 string encoded; 1485 1486 switch (_method) { 1487 case "POST","PUT","PATCH": 1488 encoded = params2query(params); 1489 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "application/x-www-form-urlencoded"); 1490 if ( encoded.length > 0) { 1491 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(encoded.length)); 1492 } 1493 req.put(requestString()); 1494 break; 1495 default: 1496 req.put(requestString(params)); 1497 } 1498 1499 h.byKeyValue. 1500 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1501 each!(h => req.put(h)); 1502 req.put("\r\n"); 1503 if ( encoded ) { 1504 req.put(encoded); 1505 } 1506 1507 debug(requests) trace(req.data); 1508 if ( _verbosity >= 1 ) { 1509 req.data.splitLines.each!(a => writeln("> " ~ a)); 1510 } 1511 // 1512 // Now send request and receive response 1513 // 1514 try { 1515 _stream.send(req.data()); 1516 _response._requestSentAt = Clock.currTime; 1517 debug(requests) trace("starting receive response"); 1518 receiveResponse(_stream); 1519 debug(requests) trace("done receive response"); 1520 _response._finishedAt = Clock.currTime; 1521 } 1522 catch (NetworkException e) { 1523 // On SEND this can means: 1524 // we started to send request to the server, but it closed connection because of keepalive timeout. 1525 // We have to restart request if possible. 1526 1527 // On RECEIVE - if we received something - then this exception is real and unexpected error. 1528 // If we didn't receive anything - we can restart request again as it can be 1529 debug(requests) tracef("Exception on receive response: %s", e.msg); 1530 if ( _response._responseHeaders.length != 0 ) 1531 { 1532 _stream.close(); 1533 throw new RequestException("Unexpected network error"); 1534 } 1535 } 1536 1537 if ( serverPrematurelyClosedConnection() 1538 && !restartedRequest 1539 && isIdempotent(_method) 1540 ) { 1541 /// 1542 /// We didn't receive any data (keepalive connectioin closed?) 1543 /// and we can restart this request. 1544 /// Go ahead. 1545 /// 1546 debug(requests) tracef("Server closed keepalive connection"); 1547 1548 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1549 1550 _cm.del(_uri.scheme, _uri.host, _uri.port); 1551 _stream.close(); 1552 _stream = null; 1553 1554 restartedRequest = true; 1555 goto connect; 1556 } 1557 1558 if ( _useStreaming ) { 1559 if ( _response._receiveAsRange.activated ) { 1560 debug(requests) trace("streaming_in activated"); 1561 return _response; 1562 } else { 1563 // this can happen if whole response body received together with headers 1564 _response._receiveAsRange.data = _response.responseBody.data; 1565 } 1566 } 1567 1568 close_connection_if_not_keepalive(_stream); 1569 1570 if ( _verbosity >= 1 ) { 1571 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1572 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1573 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1574 } 1575 1576 if ( willFollowRedirect ) { 1577 debug(requests) trace("going to follow redirect"); 1578 if ( _history.length >= _maxRedirects ) { 1579 _stream = null; 1580 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1581 } 1582 // "location" in response already checked in canFollowRedirect 1583 immutable new_location = *("location" in _response.responseHeaders); 1584 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1585 1586 if ( _method == "GET" && _response.code == 301 ) { 1587 _permanent_redirects[_uri] = new_location; 1588 } 1589 1590 // save current response for history 1591 _history ~= _response; 1592 1593 // prepare new response (for redirected request) 1594 _response = new HTTPResponse; 1595 _response.uri = current_uri; 1596 _response.finalURI = next_uri; 1597 _stream = null; 1598 1599 // set new uri 1600 _uri = next_uri; 1601 debug(requests) tracef("Redirected to %s", next_uri); 1602 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1603 // 307 and 308 do not change method 1604 return this.get(); 1605 } 1606 if ( restartedRequest ) { 1607 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1608 restartedRequest = false; 1609 } 1610 goto connect; 1611 } 1612 1613 _response._history = _history; 1614 return _response; 1615 } 1616 1617 /// WRAPPERS 1618 /// 1619 /// send file(s) using POST and multipart form. 1620 /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear. 1621 /// Parameters: 1622 /// url = url 1623 /// files = array of PostFile structures 1624 /// Returns: 1625 /// Response 1626 /// Each PostFile structure contain path to file, and optional field name and content type. 1627 /// If no field name provided, then basename of the file will be used. 1628 /// application/octet-stream is default when no content type provided. 1629 /// Example: 1630 /// --------------------------------------------------------------- 1631 /// PostFile[] files = [ 1632 /// {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 1633 /// {fileName:"tests/test.txt"} 1634 /// ]; 1635 /// rs = rq.exec!"POST"("http://httpbin.org/post", files); 1636 /// --------------------------------------------------------------- 1637 /// 1638 deprecated("Use Request() instead of HTTPRequest(); will be removed 2019-07") 1639 HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") { 1640 MultipartForm multipart; 1641 File[] toClose; 1642 foreach(ref f; files) { 1643 File file = File(f.fileName, "rb"); 1644 toClose ~= file; 1645 string fileName = f.fileName ? f.fileName : f.fieldName; 1646 string contentType = f.contentType ? f.contentType : "application/octetstream"; 1647 multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]); 1648 } 1649 auto res = exec!"POST"(url, multipart); 1650 toClose.each!"a.close"; 1651 return res; 1652 } 1653 /// 1654 /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names) 1655 /// Consider switch to exec(url, QueryParams) as it more generic and clear. 1656 /// Parameters: 1657 /// url = url 1658 /// params = dictionary with field names as keys and field values as values. 1659 /// Returns: 1660 /// Response 1661 deprecated("Use Request() instead of HTTPRequest(); will be removed 2019-07") 1662 HTTPResponse exec(string method="GET")(string url, string[string] params) { 1663 return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array); 1664 } 1665 /// 1666 /// GET request. Simple wrapper over exec!"GET" 1667 /// Params: 1668 /// args = request parameters. see exec docs. 1669 /// 1670 deprecated("Use Request() instead of HTTPRequest; will be removed 2019-07") 1671 HTTPResponse get(A...)(A args) { 1672 return exec!"GET"(args); 1673 } 1674 /// 1675 /// POST request. Simple wrapper over exec!"POST" 1676 /// Params: 1677 /// uri = endpoint uri 1678 /// args = request parameters. see exec docs. 1679 /// 1680 deprecated("Use Request() instead of HTTPRequest; will be removed 2019-07") 1681 HTTPResponse post(A...)(string uri, A args) { 1682 return exec!"POST"(uri, args); 1683 } 1684 1685 import requests.request; 1686 1687 // we use this if we send from ubyte[][] and user provided Content-Length 1688 private void sendFlattenContent(NetworkStream _stream) { 1689 while ( !_postData.empty ) { 1690 auto chunk = _postData.front; 1691 _stream.send(chunk); 1692 _postData.popFront; 1693 } 1694 debug(requests) tracef("sent"); 1695 } 1696 // we use this if we send from ubyte[][] as chunked content 1697 private void sendChunkedContent(NetworkStream _stream) { 1698 while ( !_postData.empty ) { 1699 auto chunk = _postData.front; 1700 auto chunkHeader = "%x\r\n".format(chunk.length); 1701 debug(requests) tracef("sending %s%s", chunkHeader, cast(string)chunk); 1702 _stream.send(chunkHeader); 1703 _stream.send(chunk); 1704 _stream.send("\r\n"); 1705 debug(requests) tracef("chunk sent"); 1706 _postData.popFront; 1707 } 1708 debug(requests) tracef("sent"); 1709 _stream.send("0\r\n\r\n"); 1710 } 1711 1712 HTTPResponse exec_from_range(InputRangeAdapter postData) 1713 do { 1714 1715 _postData = postData; 1716 1717 debug(requests) tracef("exec from range"); 1718 1719 NetworkStream _stream; 1720 _response = new HTTPResponse; 1721 _history.length = 0; 1722 _response.uri = _uri; 1723 _response.finalURI = _uri; 1724 bool restartedRequest = false; 1725 bool send_flat; 1726 1727 connect: 1728 _contentReceived = 0; 1729 _response._startedAt = Clock.currTime; 1730 1731 assert(_stream is null); 1732 1733 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1734 1735 if ( _stream is null ) { 1736 debug(requests) trace("create new connection"); 1737 _stream = setupConnection(); 1738 } else { 1739 debug(requests) trace("reuse old connection"); 1740 } 1741 1742 assert(_stream !is null); 1743 1744 if ( !_stream.isConnected ) { 1745 debug(requests) trace("disconnected stream on enter"); 1746 if ( !restartedRequest ) { 1747 debug(requests) trace("disconnected stream on enter: retry"); 1748 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1749 1750 _cm.del(_uri.scheme, _uri.host, _uri.port); 1751 _stream.close(); 1752 _stream = null; 1753 1754 restartedRequest = true; 1755 goto connect; 1756 } 1757 debug(requests) trace("disconnected stream on enter: return response"); 1758 //_stream = null; 1759 return _response; 1760 } 1761 _response._connectedAt = Clock.currTime; 1762 1763 Appender!string req; 1764 req.put(requestString()); 1765 1766 auto h = requestHeaders; 1767 if ( _contentType ) { 1768 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", _contentType); 1769 } 1770 1771 if ( _postData.length >= 0 ) 1772 { 1773 // we know t 1774 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(_postData.length)); 1775 } 1776 1777 if ( _userHeaders.ContentLength || "Content-Length" in h ) 1778 { 1779 debug(requests) tracef("User provided content-length for chunked content"); 1780 send_flat = true; 1781 } 1782 else 1783 { 1784 h["Transfer-Encoding"] = "chunked"; 1785 send_flat = false; 1786 } 1787 h.byKeyValue. 1788 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1789 each!(h => req.put(h)); 1790 req.put("\r\n"); 1791 1792 debug(requests) tracef("send <%s>", req.data); 1793 if ( _verbosity >= 1 ) { 1794 req.data.splitLines.each!(a => writeln("> " ~ a)); 1795 } 1796 1797 try { 1798 // send headers 1799 _stream.send(req.data()); 1800 // send body 1801 if ( send_flat ) { 1802 sendFlattenContent(_stream); 1803 } else { 1804 sendChunkedContent(_stream); 1805 } 1806 _response._requestSentAt = Clock.currTime; 1807 debug(requests) trace("starting receive response"); 1808 receiveResponse(_stream); 1809 debug(requests) trace("finished receive response"); 1810 _response._finishedAt = Clock.currTime; 1811 } 1812 catch (NetworkException e) 1813 { 1814 _stream.close(); 1815 throw new RequestException("Network error during data exchange"); 1816 } 1817 if ( serverPrematurelyClosedConnection() 1818 && !restartedRequest 1819 && isIdempotent(_method) 1820 ) { 1821 /// 1822 /// We didn't receive any data (keepalive connectioin closed?) 1823 /// and we can restart this request. 1824 /// Go ahead. 1825 /// 1826 debug(requests) tracef("Server closed keepalive connection"); 1827 1828 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1829 1830 _cm.del(_uri.scheme, _uri.host, _uri.port); 1831 _stream.close(); 1832 _stream = null; 1833 1834 restartedRequest = true; 1835 goto connect; 1836 } 1837 1838 if ( _useStreaming ) { 1839 if ( _response._receiveAsRange.activated ) { 1840 debug(requests) trace("streaming_in activated"); 1841 return _response; 1842 } else { 1843 // this can happen if whole response body received together with headers 1844 _response._receiveAsRange.data = _response.responseBody.data; 1845 } 1846 } 1847 1848 close_connection_if_not_keepalive(_stream); 1849 1850 if ( _verbosity >= 1 ) { 1851 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1852 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1853 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1854 } 1855 1856 1857 if ( willFollowRedirect ) { 1858 if ( _history.length >= _maxRedirects ) { 1859 _stream = null; 1860 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1861 } 1862 // "location" in response already checked in canFollowRedirect 1863 immutable new_location = *("location" in _response.responseHeaders); 1864 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1865 1866 immutable get_or_head = _method == "GET" || _method == "HEAD"; 1867 immutable code = _response.code; 1868 1869 // save current response for history 1870 _history ~= _response; 1871 1872 if ( code == 301 ) 1873 { 1874 // permanent redirect and change method 1875 _permanent_redirects[_uri] = new_location; 1876 if ( !get_or_head ) 1877 { 1878 _method = "GET"; 1879 } 1880 } 1881 if ( (code == 302 || code == 303) && !get_or_head) 1882 { 1883 // only change method 1884 _method = "GET"; 1885 } 1886 if ( code == 307 ) 1887 { 1888 // no change method, no permanent 1889 } 1890 if ( code == 308 ) 1891 { 1892 // permanent redirection and do not change method 1893 _permanent_redirects[_uri] = new_location; 1894 } 1895 1896 // prepare new response (for redirected request) 1897 _response = new HTTPResponse; 1898 _response.uri = current_uri; 1899 _response.finalURI = next_uri; 1900 1901 _stream = null; 1902 1903 // set new uri 1904 this._uri = next_uri; 1905 debug(requests) tracef("Redirected to %s", next_uri); 1906 if ( restartedRequest ) { 1907 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1908 restartedRequest = false; 1909 } 1910 if ( _method == "GET") 1911 { 1912 return exec_from_parameters(); 1913 } 1914 goto connect; 1915 } 1916 1917 _response._history = _history; 1918 return _response; 1919 } 1920 1921 HTTPResponse exec_from_multipart_form(MultipartForm form) { 1922 import std.uuid; 1923 import std.file; 1924 1925 _multipartForm = form; 1926 1927 debug(requests) tracef("exec from multipart form"); 1928 1929 NetworkStream _stream; 1930 _response = new HTTPResponse; 1931 _response.uri = _uri; 1932 _response.finalURI = _uri; 1933 bool restartedRequest = false; 1934 1935 connect: 1936 _contentReceived = 0; 1937 _response._startedAt = Clock.currTime; 1938 1939 assert(_stream is null); 1940 1941 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1942 1943 if ( _stream is null ) { 1944 debug(requests) trace("create new connection"); 1945 _stream = setupConnection(); 1946 } else { 1947 debug(requests) trace("reuse old connection"); 1948 } 1949 1950 assert(_stream !is null); 1951 1952 if ( !_stream.isConnected ) { 1953 debug(requests) trace("disconnected stream on enter"); 1954 if ( !restartedRequest ) { 1955 debug(requests) trace("disconnected stream on enter: retry"); 1956 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1957 1958 _cm.del(_uri.scheme, _uri.host, _uri.port); 1959 _stream.close(); 1960 _stream = null; 1961 1962 restartedRequest = true; 1963 goto connect; 1964 } 1965 debug(requests) trace("disconnected stream on enter: return response"); 1966 //_stream = null; 1967 return _response; 1968 } 1969 _response._connectedAt = Clock.currTime; 1970 1971 Appender!string req; 1972 req.put(requestString()); 1973 1974 string boundary = randomUUID().toString; 1975 string[] partHeaders; 1976 size_t contentLength; 1977 1978 foreach(ref part; _multipartForm._sources) { 1979 string h = "--" ~ boundary ~ "\r\n"; 1980 string disposition = `form-data; name="%s"`.format(part.name); 1981 string optionals = part. 1982 parameters.byKeyValue(). 1983 filter!(p => p.key!="Content-Type"). 1984 map! (p => "%s=%s".format(p.key, p.value)). 1985 join("; "); 1986 1987 h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n"; 1988 1989 auto contentType = "Content-Type" in part.parameters; 1990 if ( contentType ) { 1991 h ~= "Content-Type: " ~ *contentType ~ "\r\n"; 1992 } 1993 1994 h ~= "\r\n"; 1995 partHeaders ~= h; 1996 contentLength += h.length + part.input.getSize() + "\r\n".length; 1997 } 1998 contentLength += "--".length + boundary.length + "--\r\n".length; 1999 2000 auto h = requestHeaders(); 2001 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "multipart/form-data; boundary=" ~ boundary); 2002 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(contentLength)); 2003 2004 h.byKeyValue. 2005 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 2006 each!(h => req.put(h)); 2007 req.put("\r\n"); 2008 2009 debug(requests) trace(req.data); 2010 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 2011 2012 try { 2013 _stream.send(req.data()); 2014 foreach(ref source; _multipartForm._sources) { 2015 debug(requests) tracef("sending part headers <%s>", partHeaders.front); 2016 _stream.send(partHeaders.front); 2017 partHeaders.popFront; 2018 while (true) { 2019 auto chunk = source.input.read(); 2020 if ( chunk.length <= 0 ) { 2021 break; 2022 } 2023 _stream.send(chunk); 2024 } 2025 _stream.send("\r\n"); 2026 } 2027 _stream.send("--" ~ boundary ~ "--\r\n"); 2028 _response._requestSentAt = Clock.currTime; 2029 receiveResponse(_stream); 2030 _response._finishedAt = Clock.currTime; 2031 } 2032 catch (NetworkException e) { 2033 errorf("Error sending request: ", e.msg); 2034 _stream.close(); 2035 return _response; 2036 } 2037 2038 if ( serverPrematurelyClosedConnection() 2039 && !restartedRequest 2040 && isIdempotent(_method) 2041 ) { 2042 /// 2043 /// We didn't receive any data (keepalive connectioin closed?) 2044 /// and we can restart this request. 2045 /// Go ahead. 2046 /// 2047 debug(requests) tracef("Server closed keepalive connection"); 2048 2049 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 2050 2051 _cm.del(_uri.scheme, _uri.host, _uri.port); 2052 _stream.close(); 2053 _stream = null; 2054 2055 restartedRequest = true; 2056 goto connect; 2057 } 2058 2059 if ( _useStreaming ) { 2060 if ( _response._receiveAsRange.activated ) { 2061 debug(requests) trace("streaming_in activated"); 2062 return _response; 2063 } else { 2064 // this can happen if whole response body received together with headers 2065 _response._receiveAsRange.data = _response.responseBody.data; 2066 } 2067 } 2068 2069 close_connection_if_not_keepalive(_stream); 2070 2071 if ( _verbosity >= 1 ) { 2072 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 2073 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 2074 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 2075 } 2076 2077 if ( willFollowRedirect ) { 2078 if ( _history.length >= _maxRedirects ) { 2079 _stream = null; 2080 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 2081 } 2082 // "location" in response already checked in canFollowRedirect 2083 immutable new_location = *("location" in _response.responseHeaders); 2084 immutable current_uri = _uri; 2085 immutable next_uri = uriFromLocation(_uri, new_location); 2086 2087 immutable get_or_head = _method == "GET" || _method == "HEAD"; 2088 immutable code = _response.code; 2089 2090 // save current response for history 2091 _history ~= _response; 2092 2093 if ( code == 301 ) 2094 { 2095 // permanent redirect and change method 2096 _permanent_redirects[_uri] = new_location; 2097 if ( !get_or_head ) 2098 { 2099 _method = "GET"; 2100 } 2101 } 2102 if ( (code == 302 || code == 303) && !get_or_head) 2103 { 2104 // only change method 2105 _method = "GET"; 2106 } 2107 if ( code == 307 ) 2108 { 2109 // no change method, no permanent 2110 } 2111 if ( code == 308 ) 2112 { 2113 // permanent redirection and do not change method 2114 _permanent_redirects[_uri] = new_location; 2115 } 2116 2117 // prepare new response (for redirected request) 2118 _response = new HTTPResponse; 2119 _response.uri = current_uri; 2120 _response.finalURI = next_uri; 2121 _stream = null; 2122 2123 // set new uri 2124 this._uri = next_uri; 2125 debug(requests) tracef("Redirected to %s", next_uri); 2126 if ( restartedRequest ) { 2127 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 2128 restartedRequest = false; 2129 } 2130 if ( _method == "GET") 2131 { 2132 return exec_from_parameters(); 2133 } 2134 goto connect; 2135 } 2136 2137 _response._history = _history; 2138 return _response; 2139 } 2140 2141 HTTPResponse exec_from_parameters() { 2142 2143 debug(requests) tracef("exec from parameters request"); 2144 2145 assert(_uri != URI.init); 2146 NetworkStream _stream; 2147 _response = new HTTPResponse; 2148 _history.length = 0; 2149 _response.uri = _uri; 2150 _response.finalURI = _uri; 2151 bool restartedRequest = false; // True if this is restarted keepAlive request 2152 2153 connect: 2154 if ( _method == "GET" && _uri in _permanent_redirects ) { 2155 debug(requests) trace("use parmanent redirects cache"); 2156 _uri = uriFromLocation(_uri, _permanent_redirects[_uri]); 2157 _response._finalURI = _uri; 2158 } 2159 _contentReceived = 0; 2160 _response._startedAt = Clock.currTime; 2161 2162 assert(_stream is null); 2163 2164 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 2165 2166 if ( _stream is null ) { 2167 debug(requests) trace("create new connection"); 2168 _stream = setupConnection(); 2169 } else { 2170 debug(requests) trace("reuse old connection"); 2171 } 2172 2173 assert(_stream !is null); 2174 2175 if ( !_stream.isConnected ) { 2176 debug(requests) trace("disconnected stream on enter"); 2177 if ( !restartedRequest ) { 2178 debug(requests) trace("disconnected stream on enter: retry"); 2179 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 2180 2181 _cm.del(_uri.scheme, _uri.host, _uri.port); 2182 _stream.close(); 2183 _stream = null; 2184 2185 restartedRequest = true; 2186 goto connect; 2187 } 2188 debug(requests) trace("disconnected stream on enter: return response"); 2189 //_stream = null; 2190 return _response; 2191 } 2192 _response._connectedAt = Clock.currTime; 2193 2194 auto h = requestHeaders(); 2195 2196 Appender!string req; 2197 2198 string encoded; 2199 2200 switch (_method) { 2201 case "POST","PUT","PATCH": 2202 encoded = params2query(_params); 2203 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "application/x-www-form-urlencoded"); 2204 if ( encoded.length > 0) { 2205 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(encoded.length)); 2206 } 2207 req.put(requestString()); 2208 break; 2209 default: 2210 req.put(requestString(_params)); 2211 } 2212 2213 h.byKeyValue. 2214 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 2215 each!(h => req.put(h)); 2216 req.put("\r\n"); 2217 if ( encoded ) { 2218 req.put(encoded); 2219 } 2220 2221 debug(requests) trace(req.data); 2222 if ( _verbosity >= 1 ) { 2223 req.data.splitLines.each!(a => writeln("> " ~ a)); 2224 } 2225 // 2226 // Now send request and receive response 2227 // 2228 try { 2229 _stream.send(req.data()); 2230 _response._requestSentAt = Clock.currTime; 2231 debug(requests) trace("starting receive response"); 2232 receiveResponse(_stream); 2233 debug(requests) tracef("done receive response"); 2234 _response._finishedAt = Clock.currTime; 2235 } 2236 catch (NetworkException e) { 2237 // On SEND this can means: 2238 // we started to send request to the server, but it closed connection because of keepalive timeout. 2239 // We have to restart request if possible. 2240 2241 // On RECEIVE - if we received something - then this exception is real and unexpected error. 2242 // If we didn't receive anything - we can restart request again as it can be 2243 debug(requests) tracef("Exception on receive response: %s", e.msg); 2244 if ( _response._responseHeaders.length != 0 ) 2245 { 2246 _stream.close(); 2247 throw new RequestException("Unexpected network error"); 2248 } 2249 } 2250 2251 if ( serverPrematurelyClosedConnection() 2252 && !restartedRequest 2253 && isIdempotent(_method) 2254 ) { 2255 /// 2256 /// We didn't receive any data (keepalive connectioin closed?) 2257 /// and we can restart this request. 2258 /// Go ahead. 2259 /// 2260 debug(requests) tracef("Server closed keepalive connection"); 2261 2262 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 2263 2264 _cm.del(_uri.scheme, _uri.host, _uri.port); 2265 _stream.close(); 2266 _stream = null; 2267 2268 restartedRequest = true; 2269 goto connect; 2270 } 2271 2272 if ( _useStreaming ) { 2273 if ( _response._receiveAsRange.activated ) { 2274 debug(requests) trace("streaming_in activated"); 2275 return _response; 2276 } else { 2277 // this can happen if whole response body received together with headers 2278 _response._receiveAsRange.data = _response.responseBody.data; 2279 } 2280 } 2281 2282 close_connection_if_not_keepalive(_stream); 2283 2284 if ( _verbosity >= 1 ) { 2285 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 2286 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 2287 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 2288 } 2289 2290 if ( willFollowRedirect ) { 2291 debug(requests) trace("going to follow redirect"); 2292 if ( _history.length >= _maxRedirects ) { 2293 _stream = null; 2294 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 2295 } 2296 // "location" in response already checked in canFollowRedirect 2297 immutable new_location = *("location" in _response.responseHeaders); 2298 immutable current_uri = _uri; 2299 immutable next_uri = uriFromLocation(_uri, new_location); 2300 2301 immutable get_or_head = _method == "GET" || _method == "HEAD"; 2302 immutable code = _response.code; 2303 2304 // save current response for history 2305 _history ~= _response; 2306 2307 if ( code == 301 ) 2308 { 2309 // permanent redirect and change method 2310 _permanent_redirects[_uri] = new_location; 2311 if ( !get_or_head ) 2312 { 2313 _method = "GET"; 2314 } 2315 } 2316 if ( (code == 302 || code == 303) && !get_or_head) 2317 { 2318 // only change method 2319 _method = "GET"; 2320 } 2321 if ( code == 307 ) 2322 { 2323 // no change method, no permanent 2324 } 2325 if ( code == 308 ) 2326 { 2327 // permanent redirection and do not change method 2328 _permanent_redirects[_uri] = new_location; 2329 } 2330 2331 // prepare new response (for redirected request) 2332 _response = new HTTPResponse; 2333 _response.uri = current_uri; 2334 _response.finalURI = next_uri; 2335 _stream = null; 2336 2337 // set new uri 2338 _uri = next_uri; 2339 debug(requests) tracef("Redirected to %s", next_uri); 2340 //if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 2341 // // 307 and 308 do not change method 2342 // return exec_from_parameters(r); 2343 //} 2344 if ( restartedRequest ) { 2345 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 2346 restartedRequest = false; 2347 } 2348 goto connect; 2349 } 2350 2351 _response._history = _history; 2352 return _response; 2353 } 2354 HTTPResponse execute(Request r) 2355 { 2356 _method = r.method; 2357 _uri = r.uri; 2358 _useStreaming = r.useStreaming; 2359 _permanent_redirects = r.permanent_redirects; 2360 _maxRedirects = r.maxRedirects; 2361 _authenticator = r.authenticator; 2362 _maxHeadersLength = r.maxHeadersLength; 2363 _maxContentLength = r.maxContentLength; 2364 _verbosity = r.verbosity; 2365 _keepAlive = r.keepAlive; 2366 _bufferSize = r.bufferSize; 2367 _proxy = r.proxy; 2368 _timeout = r.timeout; 2369 _contentType = r.contentType; 2370 _socketFactory = r.socketFactory; 2371 _sslOptions = r.sslOptions; 2372 _bind = r.bind; 2373 _headers = r.headers; 2374 _userHeaders = r.userHeaders; 2375 2376 _params = r.params; 2377 2378 // this assignments increments refCounts, so we can't use const Request 2379 // but Request is anyway struct and called by-value 2380 _cm = r.cm; 2381 _cookie = r.cookie; 2382 2383 debug(requests) trace("serving %s".format(r)); 2384 if ( !r.postData.empty) 2385 { 2386 return exec_from_range(r.postData); 2387 } 2388 if ( r.hasMultipartForm ) 2389 { 2390 return exec_from_multipart_form(r.multipartForm); 2391 } 2392 auto rs = exec_from_parameters(); 2393 return rs; 2394 } 2395 } 2396 2397 version(vibeD) { 2398 import std.json; 2399 package string httpTestServer() { 2400 return "http://httpbin.org/"; 2401 } 2402 package string fromJsonArrayToStr(JSONValue v) { 2403 return v.str; 2404 } 2405 } 2406 else { 2407 import std.json; 2408 package string httpTestServer() { 2409 return "http://127.0.0.1:8081/"; 2410 } 2411 package string fromJsonArrayToStr(JSONValue v) { 2412 return cast(string)(v.array.map!"cast(ubyte)a.integer".array); 2413 } 2414 }