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