1 module requests.http; 2 3 private: 4 import std.algorithm; 5 import std.array; 6 import std.conv; 7 import std.datetime; 8 import std.exception; 9 import std.format; 10 import std.stdio; 11 import std.range; 12 import std.socket; 13 import std.string; 14 import std.traits; 15 import std.typecons; 16 import std.experimental.logger; 17 import core.thread; 18 import core.stdc.errno; 19 20 import requests.streams; 21 import requests.uri; 22 import requests.utils; 23 import requests.base; 24 25 static this() { 26 globalLogLevel(LogLevel.error); 27 } 28 29 static immutable ushort[] redirectCodes = [301, 302, 303]; 30 31 static string urlEncoded(string p) pure @safe { 32 immutable string[dchar] translationTable = [ 33 ' ': "%20", '!': "%21", '*': "%2A", '\'': "%27", '(': "%28", ')': "%29", 34 ';': "%3B", ':': "%3A", '@': "%40", '&': "%26", '=': "%3D", '+': "%2B", 35 '$': "%24", ',': "%2C", '/': "%2F", '?': "%3F", '#': "%23", '[': "%5B", 36 ']': "%5D", '%': "%25", 37 ]; 38 return p.translate(translationTable); 39 } 40 unittest { 41 assert(urlEncoded(`abc !#$&'()*+,/:;=?@[]`) == "abc%20%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"); 42 } 43 44 public class TimeoutException: Exception { 45 this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure { 46 super(msg, file, line); 47 } 48 } 49 50 public interface Auth { 51 string[string] authHeaders(string domain); 52 } 53 /** 54 * Basic authentication. 55 * Adds $(B Authorization: Basic) header to request. 56 */ 57 public class BasicAuthentication: Auth { 58 private { 59 string _username, _password; 60 string[] _domains; 61 } 62 /// Constructor. 63 /// Params: 64 /// username = username 65 /// password = password 66 /// domains = not used now 67 /// 68 this(string username, string password, string[] domains = []) { 69 _username = username; 70 _password = password; 71 _domains = domains; 72 } 73 override string[string] authHeaders(string domain) { 74 import std.base64; 75 string[string] auth; 76 auth["Authorization"] = "Basic " ~ to!string(Base64.encode(cast(ubyte[])"%s:%s".format(_username, _password))); 77 return auth; 78 } 79 } 80 81 82 /// 83 /// Response - result of request execution. 84 /// 85 /// Response.code - response HTTP code. 86 /// Response.status_line - received HTTP status line. 87 /// Response.responseHeaders - received headers. 88 /// Response.responseBody - container for received body 89 /// Response.history - for redirected responses contain all history 90 /// 91 public class HTTPResponse : Response { 92 private { 93 // ushort __code; 94 string __status_line; 95 string[string] __responseHeaders; 96 // Buffer!ubyte __responseBody; 97 HTTPResponse[] __history; // redirects history 98 SysTime __startedAt, __connectedAt, __requestSentAt, __finishedAt; 99 } 100 ~this() { 101 __responseHeaders = null; 102 __history.length = 0; 103 } 104 mixin(getter("code")); 105 mixin(getter("status_line")); 106 mixin(getter("responseHeaders")); 107 // @property auto responseBody() inout pure @safe nothrow { 108 // return __responseBody; 109 // } 110 mixin(getter("history")); 111 private { 112 mixin(setter("code")); 113 mixin(setter("status_line")); 114 mixin(setter("responseHeaders")); 115 } 116 @property auto getStats() const pure @safe { 117 alias statTuple = Tuple!(Duration, "connectTime", 118 Duration, "sendTime", 119 Duration, "recvTime"); 120 statTuple stat; 121 stat.connectTime = __connectedAt - __startedAt; 122 stat.sendTime = __requestSentAt - __connectedAt; 123 stat.recvTime = __finishedAt - __requestSentAt; 124 return stat; 125 } 126 } 127 /** 128 * Struct to send multiple files in POST request. 129 */ 130 public struct PostFile { 131 /// Path to the file to send. 132 string fileName; 133 /// Name of the field (if empty - send file base name) 134 string fieldName; 135 /// contentType of the file if not empty 136 string contentType; 137 } 138 /// 139 /// Request. 140 /// Configurable parameters: 141 /// $(B headers) - add any additional headers you'd like to send. 142 /// $(B authenticator) - class to send auth headers. 143 /// $(B keepAlive) - set true for keepAlive requests. default false. 144 /// $(B maxRedirects) - maximum number of redirects. default 10. 145 /// $(B maxHeadersLength) - maximum length of server response headers. default = 32KB. 146 /// $(B maxContentLength) - maximun content length. delault = 5MB. 147 /// $(B bufferSize) - send and receive buffer size. default = 16KB. 148 /// $(B verbosity) - level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0. 149 /// $(B proxy) - set proxy url if needed. default - null. 150 /// 151 public struct HTTPRequest { 152 private { 153 enum __preHeaders = [ 154 "Accept-Encoding": "gzip, deflate", 155 "User-Agent": "dlang-requests" 156 ]; 157 string __method = "GET"; 158 URI __uri; 159 string[string] __headers; 160 string[] __filteredHeaders; 161 Auth __authenticator; 162 bool __keepAlive = true; 163 uint __maxRedirects = 10; 164 size_t __maxHeadersLength = 32 * 1024; // 32 KB 165 size_t __maxContentLength = 5 * 1024 * 1024; // 5MB 166 ptrdiff_t __contentLength; 167 SocketStream __stream; 168 Duration __timeout = 30.seconds; 169 HTTPResponse __response; 170 HTTPResponse[] __history; // redirects history 171 size_t __bufferSize = 16*1024; // 16k 172 uint __verbosity = 0; // 0 - no output, 1 - headers, 2 - headers+body info 173 DataPipe!ubyte __bodyDecoder; 174 DecodeChunked __unChunker; 175 string __proxy; 176 } 177 178 mixin(getter("keepAlive")); 179 mixin(setter("keepAlive")); 180 mixin(getter("method")); 181 mixin(setter("method")); 182 mixin(getter("timeout")); 183 mixin(setter("timeout")); 184 mixin(setter("authenticator")); 185 mixin(getter("maxContentLength")); 186 mixin(setter("maxContentLength")); 187 mixin(getter("maxRedirects")); 188 mixin(setter("maxRedirects")); 189 mixin(getter("maxHeadersLength")); 190 mixin(setter("maxHeadersLength")); 191 mixin(getter("bufferSize")); 192 mixin(setter("bufferSize")); 193 mixin(getter("verbosity")); 194 mixin(setter("verbosity")); 195 mixin(setter("proxy")); 196 197 this(string uri) { 198 __uri = URI(uri); 199 } 200 ~this() { 201 if ( __stream && __stream.isConnected) { 202 __stream.close(); 203 } 204 __stream = null; 205 __headers = null; 206 __authenticator = null; 207 __history = null; 208 } 209 210 @property void uri(in URI newURI) { 211 handleURLChange(__uri, newURI); 212 __uri = newURI; 213 } 214 /// Add headers to request 215 /// Params: 216 /// headers = headers to send. 217 void addHeaders(in string[string] headers) { 218 foreach(pair; headers.byKeyValue) { 219 __headers[pair.key] = pair.value; 220 } 221 } 222 /// Remove headers from request 223 /// Params: 224 /// headers = headers to remove. 225 void removeHeaders(in string[] headers) pure { 226 __filteredHeaders ~= headers; 227 } 228 /// 229 /// compose headers to send 230 /// 231 private @property string[string] headers() { 232 string[string] generatedHeaders = __preHeaders; 233 234 if ( __authenticator ) { 235 __authenticator. 236 authHeaders(__uri.host). 237 byKeyValue. 238 each!(pair => generatedHeaders[pair.key] = pair.value); 239 } 240 241 generatedHeaders["Connection"] = __keepAlive?"Keep-Alive":"Close"; 242 generatedHeaders["Host"] = __uri.host; 243 244 if ( __uri.scheme !in standard_ports || __uri.port != standard_ports[__uri.scheme] ) { 245 generatedHeaders["Host"] ~= ":%d".format(__uri.port); 246 } 247 248 __headers.byKey.each!(h => generatedHeaders[h] = __headers[h]); 249 250 __filteredHeaders.each!(h => generatedHeaders.remove(h)); 251 252 return generatedHeaders; 253 } 254 /// 255 /// Build request string. 256 /// Handle proxy and query parameters. 257 /// 258 private @property string requestString(string[string] params = null) { 259 if ( __proxy ) { 260 return "%s %s HTTP/1.1\r\n".format(__method, __uri.uri); 261 } 262 auto query = __uri.query.dup; 263 if ( params ) { 264 query ~= params2query(params); 265 if ( query[0] != '?' ) { 266 query = "?" ~ query; 267 } 268 } 269 return "%s %s%s HTTP/1.1\r\n".format(__method, __uri.path, query); 270 } 271 /// 272 /// encode parameters and build query part of the url 273 /// 274 private static string params2query(string[string] params) { 275 auto m = params.keys. 276 sort(). 277 map!(a=>urlEncoded(a) ~ "=" ~ urlEncoded(params[a])). 278 join("&"); 279 return m; 280 } 281 unittest { 282 assert(HTTPRequest.params2query(["c ":"d", "a":"b"])=="a=b&c%20=d"); 283 } 284 /// 285 /// Analyze received headers, take appropriate actions: 286 /// check content length, attach unchunk and uncompress 287 /// 288 private void analyzeHeaders(in string[string] headers) { 289 290 __contentLength = -1; 291 __unChunker = null; 292 auto contentLength = "content-length" in headers; 293 if ( contentLength ) { 294 try { 295 __contentLength = to!ptrdiff_t(*contentLength); 296 if ( __contentLength > maxContentLength) { 297 throw new RequestException("ContentLength > maxContentLength (%d>%d)". 298 format(__contentLength, __maxContentLength)); 299 } 300 } catch (ConvException e) { 301 throw new RequestException("Can't convert Content-Length from %s".format(*contentLength)); 302 } 303 } 304 auto transferEncoding = "transfer-encoding" in headers; 305 if ( transferEncoding ) { 306 tracef("transferEncoding: %s", *transferEncoding); 307 if ( *transferEncoding == "chunked") { 308 __unChunker = new DecodeChunked(); 309 __bodyDecoder.insert(__unChunker); 310 } 311 } 312 auto contentEncoding = "content-encoding" in headers; 313 if ( contentEncoding ) switch (*contentEncoding) { 314 default: 315 throw new RequestException("Unknown content-encoding " ~ *contentEncoding); 316 case "gzip": 317 case "deflate": 318 __bodyDecoder.insert(new Decompressor!ubyte); 319 } 320 } 321 /// 322 /// Called when we know that all headers already received in buffer 323 /// 1. Split headers on lines 324 /// 2. store status line, store response code 325 /// 3. unfold headers if needed 326 /// 4. store headers 327 /// 328 private void parseResponseHeaders(ref Buffer!ubyte buffer) { 329 string lastHeader; 330 foreach(line; buffer.data!(string).split("\n").map!(l => l.stripRight)) { 331 if ( ! __response.status_line.length ) { 332 tracef("statusLine: %s", line); 333 __response.status_line = line; 334 if ( __verbosity >= 1 ) { 335 writefln("< %s", line); 336 } 337 auto parsed = line.split(" "); 338 if ( parsed.length >= 3 ) { 339 __response.code = parsed[1].to!ushort; 340 } 341 continue; 342 } 343 if ( line[0] == ' ' || line[0] == '\t' ) { 344 // unfolding https://tools.ietf.org/html/rfc822#section-3.1 345 auto stored = lastHeader in __response.__responseHeaders; 346 if ( stored ) { 347 *stored ~= line; 348 } 349 continue; 350 } 351 auto parsed = line.findSplit(":"); 352 auto header = parsed[0].toLower; 353 auto value = parsed[2].strip; 354 auto stored = __response.responseHeaders.get(header, null); 355 if ( stored ) { 356 value = stored ~ ", " ~ value; 357 } 358 __response.__responseHeaders[header] = value; 359 if ( __verbosity >= 1 ) { 360 writefln("< %s: %s", parsed[0], value); 361 } 362 363 tracef("Header %s = %s", header, value); 364 lastHeader = header; 365 } 366 } 367 368 /// 369 /// Do we received \r\n\r\n? 370 /// 371 private bool headersHaveBeenReceived(in ubyte[] data, ref Buffer!ubyte buffer, out string separator) pure const @safe { 372 foreach(s; ["\r\n\r\n", "\n\n"]) { 373 if ( data.canFind(s) || buffer.canFind(s) ) { 374 separator = s; 375 return true; 376 } 377 } 378 return false; 379 } 380 381 private bool followRedirectResponse() { 382 if ( __history.length >= __maxRedirects ) { 383 return false; 384 } 385 auto location = "location" in __response.responseHeaders; 386 if ( !location ) { 387 return false; 388 } 389 __history ~= __response; 390 auto connection = "connection" in __response.__responseHeaders; 391 if ( !connection || *connection == "close" ) { 392 tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 393 __stream.close(); 394 } 395 URI oldURI = __uri; 396 URI newURI = oldURI; 397 try { 398 newURI = URI(*location); 399 } catch (UriException e) { 400 trace("Can't parse Location:, try relative uri"); 401 newURI.path = *location; 402 newURI.uri = newURI.recalc_uri; 403 } 404 handleURLChange(oldURI, newURI); 405 oldURI = __response.URI; 406 __uri = newURI; 407 __response = new HTTPResponse; 408 __response.URI = oldURI; 409 __response.finalURI = newURI; 410 return true; 411 } 412 /// 413 /// If uri changed so that we have to change host or port, then we have to close socket stream 414 /// 415 private void handleURLChange(in URI from, in URI to) { 416 if ( __stream !is null && __stream.isConnected && 417 ( from.scheme != to.scheme || from.host != to.host || from.port != to.port) ) { 418 tracef("Have to reopen stream, because of URI change"); 419 __stream.close(); 420 } 421 } 422 423 private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) { 424 if (url is null && __uri.uri == "" ) { 425 throw new RequestException("No url configured", file, line); 426 } 427 428 if ( url !is null ) { 429 URI newURI = URI(url); 430 handleURLChange(__uri, newURI); 431 __uri = newURI; 432 } 433 } 434 /// 435 /// Setup connection. Handle proxy and https case 436 /// 437 private void setupConnection() { 438 if ( !__stream || !__stream.isConnected ) { 439 tracef("Set up new connection"); 440 URI uri; 441 if ( __proxy ) { 442 // use proxy uri to connect 443 uri.uri_parse(__proxy); 444 } else { 445 // use original uri 446 uri = __uri; 447 } 448 final switch (uri.scheme) { 449 case "http": 450 __stream = new TCPSocketStream().connect(uri.host, uri.port, __timeout); 451 break; 452 case "https": 453 __stream = new SSLSocketStream().connect(uri.host, uri.port, __timeout); 454 break; 455 } 456 } else { 457 tracef("Use old connection"); 458 } 459 } 460 /// 461 /// Receive response after request we sent. 462 /// Find headers, split on headers and body, continue to receive body 463 /// 464 private void receiveResponse() { 465 466 __stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout); 467 scope(exit) { 468 __stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, 0.seconds); 469 } 470 471 __bodyDecoder = new DataPipe!ubyte(); 472 auto b = new ubyte[__bufferSize]; 473 scope(exit) { 474 __bodyDecoder = null; 475 __unChunker = null; 476 b = null; 477 } 478 479 auto buffer = Buffer!ubyte(); 480 Buffer!ubyte ResponseHeaders, partialBody; 481 size_t receivedBodyLength; 482 ptrdiff_t read; 483 string separator; 484 485 while(true) { 486 487 read = __stream.receive(b); 488 tracef("read: %d", read); 489 if ( read < 0 ) { 490 version(Windows) { 491 if ( errno == 0 ) { 492 throw new TimeoutException("Timeout receiving headers"); 493 } 494 } 495 version(Posix) { 496 if ( errno == EINTR ) { 497 continue; 498 } 499 if ( errno == EAGAIN ) { 500 throw new TimeoutException("Timeout receiving headers"); 501 } 502 throw new ErrnoException("receiving Headers"); 503 } 504 } 505 if ( read == 0 ) { 506 break; 507 } 508 509 auto data = b[0..read]; 510 buffer.put(data); 511 if ( buffer.length > maxHeadersLength ) { 512 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength)); 513 } 514 if ( headersHaveBeenReceived(data, buffer, separator) ) { 515 auto s = buffer.data!(ubyte[]).findSplit(separator); 516 ResponseHeaders = Buffer!ubyte(s[0]); 517 partialBody = Buffer!ubyte(s[2]); 518 receivedBodyLength += partialBody.length; 519 parseResponseHeaders(ResponseHeaders); 520 break; 521 } 522 } 523 524 analyzeHeaders(__response.__responseHeaders); 525 __bodyDecoder.put(partialBody); 526 527 if ( __verbosity >= 2 ) { 528 writefln("< %d bytes of body received", partialBody.length); 529 } 530 531 if ( __method == "HEAD" ) { 532 // HEAD response have ContentLength, but have no body 533 return; 534 } 535 536 while( true ) { 537 if ( __contentLength >= 0 && receivedBodyLength >= __contentLength ) { 538 trace("Body received."); 539 break; 540 } 541 if ( __unChunker && __unChunker.done ) { 542 break; 543 } 544 read = __stream.receive(b); 545 if ( read < 0 ) { 546 version(Posix) { 547 if ( errno == EINTR ) { 548 continue; 549 } 550 } 551 if ( errno == EAGAIN ) { 552 throw new TimeoutException("Timeout receiving body"); 553 } 554 throw new ErrnoException("receiving body"); 555 } 556 if ( __verbosity >= 2 ) { 557 writefln("< %d bytes of body received", read); 558 } 559 tracef("read: %d", read); 560 if ( read == 0 ) { 561 trace("read done"); 562 break; 563 } 564 receivedBodyLength += read; 565 __bodyDecoder.put(b[0..read].dup); 566 __response.__responseBody.put(__bodyDecoder.get()); 567 tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", receivedBodyLength, __contentLength, __response.__responseBody.length); 568 } 569 __bodyDecoder.flush(); 570 __response.__responseBody.put(__bodyDecoder.get()); 571 } 572 /// 573 /// execute POST request. 574 /// Send form-urlencoded data 575 /// 576 /// Parameters: 577 /// url = url to request 578 /// rqData = data to send 579 /// Returns: 580 /// Response 581 /// Examples: 582 /// ------------------------------------------------------------------ 583 /// rs = rq.exec!"POST"("http://httpbin.org/post", ["a":"b", "c":"d"]); 584 /// ------------------------------------------------------------------ 585 /// 586 HTTPResponse exec(string method)(string url, string[string] rqData) if (method=="POST") { 587 // 588 // application/x-www-form-urlencoded 589 // 590 __method = method; 591 592 __response = new HTTPResponse; 593 checkURL(url); 594 __response.URI = __uri; 595 __response.finalURI = __uri; 596 597 connect: 598 __response.__startedAt = Clock.currTime; 599 setupConnection(); 600 601 if ( !__stream.isConnected() ) { 602 return __response; 603 } 604 __response.__connectedAt = Clock.currTime; 605 606 string encoded = params2query(rqData); 607 auto h = headers; 608 h["Content-Type"] = "application/x-www-form-urlencoded"; 609 h["Content-Length"] = to!string(encoded.length); 610 611 Appender!string req; 612 req.put(requestString()); 613 h.byKeyValue. 614 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 615 each!(h => req.put(h)); 616 req.put("\r\n"); 617 req.put(encoded); 618 trace(req.data); 619 620 if ( __verbosity >= 1 ) { 621 req.data.splitLines.each!(a => writeln("> " ~ a)); 622 } 623 624 auto rc = __stream.send(req.data()); 625 if ( rc == -1 ) { 626 errorf("Error sending request: ", lastSocketError); 627 return __response; 628 } 629 __response.__requestSentAt = Clock.currTime; 630 631 receiveResponse(); 632 633 __response.__finishedAt = Clock.currTime; 634 635 auto connection = "connection" in __response.__responseHeaders; 636 if ( !connection || *connection == "close" ) { 637 tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 638 __stream.close(); 639 } 640 if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) { 641 if ( __method != "GET" ) { 642 return this.get(); 643 } 644 goto connect; 645 } 646 __response.__history = __history; 647 return __response; 648 } 649 /// 650 /// send file(s) using POST 651 /// Parameters: 652 /// url = url 653 /// files = array of PostFile structures 654 /// Returns: 655 /// Response 656 /// Example: 657 /// --------------------------------------------------------------- 658 /// PostFile[] files = [ 659 /// {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 660 /// {fileName:"tests/test.txt"} 661 /// ]; 662 /// rs = rq.exec!"POST"("http://httpbin.org/post", files); 663 /// --------------------------------------------------------------- 664 /// 665 HTTPResponse exec(string method="POST")(string url, PostFile[] files) { 666 import std.uuid; 667 import std.file; 668 // 669 // application/json 670 // 671 bool restartedRequest = false; 672 673 __method = method; 674 675 __response = new HTTPResponse; 676 checkURL(url); 677 __response.URI = __uri; 678 __response.finalURI = __uri; 679 680 connect: 681 __response.__startedAt = Clock.currTime; 682 setupConnection(); 683 684 if ( !__stream.isConnected() ) { 685 return __response; 686 } 687 __response.__connectedAt = Clock.currTime; 688 689 Appender!string req; 690 req.put(requestString()); 691 692 string boundary = randomUUID().toString; 693 string[] partHeaders; 694 size_t contentLength; 695 696 foreach(part; files) { 697 string fieldName = part.fieldName ? part.fieldName : part.fileName; 698 string h = "--" ~ boundary ~ "\r\n"; 699 h ~= `Content-Disposition: form-data; name="%s"; filename="%s"`. 700 format(fieldName, part.fileName) ~ "\r\n"; 701 if ( part.contentType ) { 702 h ~= "Content-Type: " ~ part.contentType ~ "\r\n"; 703 } 704 h ~= "\r\n"; 705 partHeaders ~= h; 706 contentLength += h.length + getSize(part.fileName) + "\r\n".length; 707 } 708 contentLength += "--".length + boundary.length + "--\r\n".length; 709 710 auto h = headers; 711 h["Content-Type"] = "multipart/form-data; boundary=" ~ boundary; 712 h["Content-Length"] = to!string(contentLength); 713 h.byKeyValue. 714 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 715 each!(h => req.put(h)); 716 req.put("\r\n"); 717 718 trace(req.data); 719 if ( __verbosity >= 1 ) { 720 req.data.splitLines.each!(a => writeln("> " ~ a)); 721 } 722 723 auto rc = __stream.send(req.data()); 724 if ( rc == -1 ) { 725 errorf("Error sending request: ", lastSocketError); 726 return __response; 727 } 728 foreach(hdr, f; zip(partHeaders, files)) { 729 tracef("sending part headers <%s>", hdr); 730 __stream.send(hdr); 731 auto file = File(f.fileName, "rb"); 732 scope(exit) { 733 file.close(); 734 } 735 foreach(chunk; file.byChunk(16*1024)) { 736 __stream.send(chunk); 737 } 738 __stream.send("\r\n"); 739 } 740 __stream.send("--" ~ boundary ~ "--\r\n"); 741 __response.__requestSentAt = Clock.currTime; 742 743 receiveResponse(); 744 745 if ( __response.__responseHeaders.length == 0 746 && __keepAlive 747 && !restartedRequest 748 && __method == "GET" 749 ) { 750 tracef("Server closed keepalive connection"); 751 __stream.close(); 752 restartedRequest = true; 753 goto connect; 754 } 755 756 __response.__finishedAt = Clock.currTime; 757 /// 758 auto connection = "connection" in __response.__responseHeaders; 759 if ( !connection || *connection == "close" ) { 760 tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 761 __stream.close(); 762 } 763 if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) { 764 if ( __method != "GET" ) { 765 return this.get(); 766 } 767 goto connect; 768 } 769 __response.__history = __history; 770 /// 771 return __response; 772 } 773 /// 774 /// POST data from some string(with Content-Length), or from range of strings (use Transfer-Encoding: chunked) 775 /// 776 /// Parameters: 777 /// url = url 778 /// content = string or input range 779 /// contentType = content type 780 /// Returns: 781 /// Response 782 /// Examples: 783 /// --------------------------------------------------------------------------------------------------------- 784 /// rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 785 /// 786 /// auto s = lineSplitter("one,\ntwo,\nthree."); 787 /// rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 788 /// 789 /// auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 790 /// rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 791 /// 792 /// auto f = File("tests/test.txt", "rb"); 793 /// rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 794 /// -------------------------------------------------------------------------------------------------------- 795 HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="text/html") 796 if ( (rank!R == 1) //isSomeString!R 797 || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 798 || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte))) 799 ) 800 { 801 // 802 // application/json 803 // 804 bool restartedRequest = false; 805 806 __method = method; 807 808 __response = new HTTPResponse; 809 checkURL(url); 810 __response.URI = __uri; 811 __response.finalURI = __uri; 812 813 connect: 814 __response.__startedAt = Clock.currTime; 815 setupConnection(); 816 817 if ( !__stream.isConnected() ) { 818 return __response; 819 } 820 __response.__connectedAt = Clock.currTime; 821 822 Appender!string req; 823 req.put(requestString()); 824 825 auto h = headers; 826 h["Content-Type"] = contentType; 827 static if ( rank!R == 1 ) { 828 h["Content-Length"] = to!string(content.length); 829 } else { 830 h["Transfer-Encoding"] = "chunked"; 831 } 832 h.byKeyValue. 833 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 834 each!(h => req.put(h)); 835 req.put("\r\n"); 836 837 trace(req.data); 838 if ( __verbosity >= 1 ) { 839 req.data.splitLines.each!(a => writeln("> " ~ a)); 840 } 841 842 auto rc = __stream.send(req.data()); 843 if ( rc == -1 ) { 844 errorf("Error sending request: ", lastSocketError); 845 return __response; 846 } 847 848 static if ( rank!R == 1) { 849 __stream.send(content); 850 } else { 851 while ( !content.empty ) { 852 auto chunk = content.front; 853 auto chunkHeader = "%x\r\n".format(chunk.length); 854 tracef("sending %s%s", chunkHeader, chunk); 855 __stream.send(chunkHeader); 856 __stream.send(chunk); 857 __stream.send("\r\n"); 858 content.popFront; 859 } 860 tracef("sent"); 861 __stream.send("0\r\n\r\n"); 862 } 863 __response.__requestSentAt = Clock.currTime; 864 865 receiveResponse(); 866 867 if ( __response.__responseHeaders.length == 0 868 && __keepAlive 869 && !restartedRequest 870 && __method == "GET" 871 ) { 872 tracef("Server closed keepalive connection"); 873 __stream.close(); 874 restartedRequest = true; 875 goto connect; 876 } 877 878 __response.__finishedAt = Clock.currTime; 879 880 /// 881 auto connection = "connection" in __response.__responseHeaders; 882 if ( !connection || *connection == "close" ) { 883 tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 884 __stream.close(); 885 } 886 if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) { 887 if ( __method != "GET" ) { 888 return this.get(); 889 } 890 goto connect; 891 } 892 /// 893 __response.__history = __history; 894 return __response; 895 } 896 /// 897 /// Send request without data 898 /// Request parameters will be encoded into request string 899 /// Parameters: 900 /// url = url 901 /// params = request parameters 902 /// Returns: 903 /// Response 904 /// Examples: 905 /// --------------------------------------------------------------------------------- 906 /// rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]); 907 /// --------------------------------------------------------------------------------- 908 /// 909 HTTPResponse exec(string method="GET")(string url = null, string[string] params = null) if (method != "POST") 910 { 911 912 __method = method; 913 __response = new HTTPResponse; 914 __history.length = 0; 915 bool restartedRequest = false; // True if this is restarted keepAlive request 916 917 checkURL(url); 918 __response.URI = __uri; 919 __response.finalURI = __uri; 920 connect: 921 __response.__startedAt = Clock.currTime; 922 setupConnection(); 923 924 if ( !__stream.isConnected() ) { 925 return __response; 926 } 927 __response.__connectedAt = Clock.currTime; 928 929 Appender!string req; 930 req.put(requestString(params)); 931 headers.byKeyValue. 932 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 933 each!(h => req.put(h)); 934 req.put("\r\n"); 935 trace(req.data); 936 937 if ( __verbosity >= 1 ) { 938 req.data.splitLines.each!(a => writeln("> " ~ a)); 939 } 940 auto rc = __stream.send(req.data()); 941 if ( rc == -1 ) { 942 errorf("Error sending request: ", lastSocketError); 943 return __response; 944 } 945 __response.__requestSentAt = Clock.currTime; 946 947 receiveResponse(); 948 949 if ( __response.__responseHeaders.length == 0 950 && __keepAlive 951 && !restartedRequest 952 && __method == "GET" 953 ) { 954 tracef("Server closed keepalive connection"); 955 __stream.close(); 956 restartedRequest = true; 957 goto connect; 958 } 959 __response.__finishedAt = Clock.currTime; 960 961 /// 962 auto connection = "connection" in __response.__responseHeaders; 963 if ( !connection || *connection == "close" ) { 964 tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 965 __stream.close(); 966 } 967 if ( __verbosity >= 1 ) { 968 writeln(">> Connect time: ", __response.__connectedAt - __response.__startedAt); 969 writeln(">> Request send time: ", __response.__requestSentAt - __response.__connectedAt); 970 writeln(">> Response recv time: ", __response.__finishedAt - __response.__requestSentAt); 971 } 972 if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) { 973 if ( __method != "GET" ) { 974 return this.get(); 975 } 976 goto connect; 977 } 978 /// 979 __response.__history = __history; 980 return __response; 981 } 982 /// 983 /// GET request. Simple wrapper over exec!"GET" 984 /// Params: 985 /// args = request parameters. see exec docs. 986 /// 987 HTTPResponse get(A...)(A args) { 988 return exec!"GET"(args); 989 } 990 /// 991 /// POST request. Simple wrapper over exec!"POST" 992 /// Params: 993 /// args = request parameters. see exec docs. 994 /// 995 HTTPResponse post(A...)(string uri, A args) { 996 return exec!"POST"(uri, args); 997 } 998 } 999 1000 /// 1001 public unittest { 1002 import std.json; 1003 globalLogLevel(LogLevel.info); 1004 tracef("http tests - start"); 1005 1006 auto rq = HTTPRequest(); 1007 auto rs = rq.get("https://httpbin.org/"); 1008 assert(rs.code==200); 1009 assert(rs.responseBody.length > 0); 1010 rs = HTTPRequest().get("http://httpbin.org/get", ["c":" d", "a":"b"]); 1011 assert(rs.code == 200); 1012 auto json = parseJSON(rs.responseBody.data).object["args"].object; 1013 assert(json["c"].str == " d"); 1014 assert(json["a"].str == "b"); 1015 1016 globalLogLevel(LogLevel.info); 1017 rq = HTTPRequest(); 1018 rq.keepAlive = true; 1019 // handmade json 1020 info("Check POST json"); 1021 rs = rq.post("http://httpbin.org/post?b=x", `{"a":"☺ ", "c":[1,2,3]}`, "application/json"); 1022 assert(rs.code==200); 1023 json = parseJSON(rs.responseBody.data).object["args"].object; 1024 assert(json["b"].str == "x"); 1025 json = parseJSON(rs.responseBody.data).object["json"].object; 1026 assert(json["a"].str == "☺ "); 1027 assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]); 1028 { 1029 import std.file; 1030 import std.path; 1031 auto tmpd = tempDir(); 1032 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1033 auto f = File(tmpfname, "wb"); 1034 f.rawWrite("abcdefgh\n12345678\n"); 1035 f.close(); 1036 // files 1037 globalLogLevel(LogLevel.info); 1038 info("Check POST files"); 1039 PostFile[] files = [ 1040 {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 1041 {fileName: tmpfname} 1042 ]; 1043 rs = rq.post("http://httpbin.org/post", files); 1044 assert(rs.code==200); 1045 info("Check POST chunked from file.byChunk"); 1046 f = File(tmpfname, "rb"); 1047 rs = rq.post("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 1048 assert(rs.code==200); 1049 auto data = parseJSON(rs.responseBody.data).object["data"].str; 1050 assert(data=="abcdefgh\n12345678\n"); 1051 f.close(); 1052 } 1053 { 1054 // string 1055 info("Check POST utf8 string"); 1056 rs = rq.post("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 1057 assert(rs.code==200); 1058 auto data = parseJSON(rs.responseBody.data).object["data"].str; 1059 assert(data=="привiт, свiт!"); 1060 } 1061 // ranges 1062 { 1063 info("Check POST chunked from lineSplitter"); 1064 auto s = lineSplitter("one,\ntwo,\nthree."); 1065 rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 1066 assert(rs.code==200); 1067 auto data = parseJSON(rs.responseBody.toString).object["data"].str; 1068 assert(data=="one,two,three."); 1069 } 1070 { 1071 info("Check POST chunked from array"); 1072 auto s = ["one,", "two,", "three."]; 1073 rs = rq.post("http://httpbin.org/post", s, "application/octet-stream"); 1074 assert(rs.code==200); 1075 auto data = parseJSON(rs.responseBody.data).object["data"].str; 1076 assert(data=="one,two,three."); 1077 } 1078 { 1079 info("Check POST chunked using std.range.chunks()"); 1080 auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1081 rs = rq.post("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 1082 assert(rs.code==200); 1083 auto data = parseJSON(rs.responseBody.data).object["data"].str; 1084 assert(data==s); 1085 } 1086 // associative array 1087 rs = rq.post("http://httpbin.org/post", ["a":"b ", "c":"d"]); 1088 assert(rs.code==200); 1089 auto form = parseJSON(rs.responseBody.data).object["form"].object; 1090 assert(form["a"].str == "b "); 1091 assert(form["c"].str == "d"); 1092 info("Check HEAD"); 1093 rs = rq.exec!"HEAD"("http://httpbin.org/"); 1094 assert(rs.code==200); 1095 info("Check DELETE"); 1096 rs = rq.exec!"DELETE"("http://httpbin.org/delete"); 1097 assert(rs.code==200); 1098 info("Check PUT"); 1099 rs = rq.exec!"PUT"("http://httpbin.org/put", `{"a":"b", "c":[1,2,3]}`, "application/json"); 1100 assert(rs.code==200); 1101 info("Check PATCH"); 1102 rs = rq.exec!"PATCH"("http://httpbin.org/patch", "привiт, свiт!", "application/octet-stream"); 1103 assert(rs.code==200); 1104 1105 info("Check compressed content"); 1106 globalLogLevel(LogLevel.info); 1107 rq = HTTPRequest(); 1108 rq.keepAlive = true; 1109 rs = rq.get("http://httpbin.org/gzip"); 1110 assert(rs.code==200); 1111 info("gzip - ok"); 1112 rs = rq.get("http://httpbin.org/deflate"); 1113 assert(rs.code==200); 1114 info("deflate - ok"); 1115 1116 info("Check redirects"); 1117 globalLogLevel(LogLevel.info); 1118 rq = HTTPRequest(); 1119 rq.keepAlive = true; 1120 rs = rq.get("http://httpbin.org/relative-redirect/2"); 1121 assert(rs.history.length == 2); 1122 assert(rs.code==200); 1123 // rq = Request(); 1124 // rq.keepAlive = true; 1125 // rq.proxy = "http://localhost:8888/"; 1126 rs = rq.get("http://httpbin.org/absolute-redirect/2"); 1127 assert(rs.history.length == 2); 1128 assert(rs.code==200); 1129 // rq = Request(); 1130 rq.maxRedirects = 2; 1131 rq.keepAlive = false; 1132 rs = rq.get("https://httpbin.org/absolute-redirect/3"); 1133 assert(rs.history.length == 2); 1134 assert(rs.code==302); 1135 1136 info("Check utf8 content"); 1137 globalLogLevel(LogLevel.info); 1138 rq = HTTPRequest(); 1139 rs = rq.get("http://httpbin.org/encoding/utf8"); 1140 assert(rs.code==200); 1141 1142 info("Check chunked content"); 1143 globalLogLevel(LogLevel.info); 1144 rq = HTTPRequest(); 1145 rq.keepAlive = true; 1146 rq.bufferSize = 16*1024; 1147 rs = rq.get("http://httpbin.org/range/1024"); 1148 assert(rs.code==200); 1149 assert(rs.responseBody.length==1024); 1150 1151 info("Check basic auth"); 1152 globalLogLevel(LogLevel.info); 1153 rq = HTTPRequest(); 1154 rq.authenticator = new BasicAuthentication("user", "passwd"); 1155 rs = rq.get("http://httpbin.org/basic-auth/user/passwd"); 1156 assert(rs.code==200); 1157 1158 globalLogLevel(LogLevel.info); 1159 info("Check exception handling, error messages are OK"); 1160 rq = HTTPRequest(); 1161 rq.timeout = 1.seconds; 1162 assertThrown!TimeoutException(rq.get("http://httpbin.org/delay/3")); 1163 assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/")); 1164 assertThrown!ConnectError(rq.get("http://1.1.1.1/")); 1165 //assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/")); 1166 1167 globalLogLevel(LogLevel.info); 1168 info("Check limits"); 1169 rq = HTTPRequest(); 1170 rq.maxContentLength = 1; 1171 assertThrown!RequestException(rq.get("http://httpbin.org/")); 1172 rq = HTTPRequest(); 1173 rq.maxHeadersLength = 1; 1174 assertThrown!RequestException(rq.get("http://httpbin.org/")); 1175 tracef("http tests - ok"); 1176 }