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