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.string; 13 import std.traits; 14 import std.typecons; 15 import std.experimental.logger; 16 import core.thread; 17 18 import requests.streams; 19 import requests.uri; 20 import requests.utils; 21 import requests.base; 22 23 static immutable ushort[] redirectCodes = [301, 302, 303]; 24 static immutable uint defaultBufferSize = 12*1024; 25 26 public class MaxRedirectsException: Exception { 27 this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow { 28 super(message, file, line, next); 29 } 30 } 31 32 public interface Auth { 33 string[string] authHeaders(string domain); 34 } 35 /** 36 * Basic authentication. 37 * Adds $(B Authorization: Basic) header to request. 38 */ 39 public class BasicAuthentication: Auth { 40 private { 41 string _username, _password; 42 string[] _domains; 43 } 44 /// Constructor. 45 /// Params: 46 /// username = username 47 /// password = password 48 /// domains = not used now 49 /// 50 this(string username, string password, string[] domains = []) { 51 _username = username; 52 _password = password; 53 _domains = domains; 54 } 55 override string[string] authHeaders(string domain) { 56 import std.base64; 57 string[string] auth; 58 auth["Authorization"] = "Basic " ~ to!string(Base64.encode(cast(ubyte[])"%s:%s".format(_username, _password))); 59 return auth; 60 } 61 } 62 /// 63 /// 64 /// 65 public auto queryParams(T...)(T params) pure nothrow @safe 66 { 67 static assert (T.length % 2 == 0, "wrong args count"); 68 69 QueryParam[] output; 70 output.reserve = T.length / 2; 71 72 void queryParamsHelper(T...)(T params, ref QueryParam[] output) 73 { 74 static if (T.length > 0) 75 { 76 output ~= QueryParam(params[0].to!string, params[1].to!string); 77 queryParamsHelper(params[2..$], output); 78 } 79 } 80 81 queryParamsHelper(params, output); 82 return output; 83 } 84 85 /// 86 /// Response - result of request execution. 87 /// 88 /// Response.code - response HTTP code. 89 /// Response.status_line - received HTTP status line. 90 /// Response.responseHeaders - received headers. 91 /// Response.responseBody - container for received body 92 /// Response.history - for redirected responses contain all history 93 /// 94 public class HTTPResponse : Response { 95 private { 96 string _status_line; 97 98 HTTPResponse[] _history; // redirects history 99 SysTime _startedAt, _connectedAt, _requestSentAt, _finishedAt; 100 101 mixin(Setter!string("status_line")); 102 } 103 104 ~this() { 105 _responseHeaders = null; 106 _history.length = 0; 107 } 108 109 mixin(Getter!string("status_line")); 110 @property final string[string] responseHeaders() @safe @nogc nothrow { 111 return _responseHeaders; 112 } 113 @property final HTTPResponse[] history() @safe @nogc nothrow { 114 return _history; 115 } 116 117 @property auto getStats() const pure @safe { 118 alias statTuple = Tuple!(Duration, "connectTime", 119 Duration, "sendTime", 120 Duration, "recvTime"); 121 statTuple stat; 122 stat.connectTime = _connectedAt - _startedAt; 123 stat.sendTime = _requestSentAt - _connectedAt; 124 stat.recvTime = _finishedAt - _requestSentAt; 125 return stat; 126 } 127 } 128 /** 129 * Struct to send multiple files in POST request. 130 */ 131 public struct PostFile { 132 /// Path to the file to send. 133 string fileName; 134 /// Name of the field (if empty - send file base name) 135 string fieldName; 136 /// contentType of the file if not empty 137 string contentType; 138 } 139 /// 140 /// This is File-like interface for sending data to multipart fotms 141 /// 142 public interface FiniteReadable { 143 /// size of the content 144 abstract ulong getSize(); 145 /// file-like read() 146 abstract ubyte[] read(); 147 } 148 /// 149 /// Helper to create form elements from File. 150 /// Params: 151 /// name = name of the field in form 152 /// f = opened std.stio.File to send to server 153 /// parameters = optional parameters (most important are "filename" and "Content-Type") 154 /// 155 public auto formData(string name, File f, string[string] parameters = null) { 156 return MultipartForm.FormData(name, new FormDataFile(f), parameters); 157 } 158 /// 159 /// Helper to create form elements from ubyte[]. 160 /// Params: 161 /// name = name of the field in form 162 /// b = data to send to server 163 /// parameters = optional parameters (can be "filename" and "Content-Type") 164 /// 165 public auto formData(string name, ubyte[] b, string[string] parameters = null) { 166 return MultipartForm.FormData(name, new FormDataBytes(b), parameters); 167 } 168 public auto formData(string name, string b, string[string] parameters = null) { 169 return MultipartForm.FormData(name, new FormDataBytes(b.dup.representation), parameters); 170 } 171 public class FormDataBytes : FiniteReadable { 172 private { 173 ulong _size; 174 ubyte[] _data; 175 size_t _offset; 176 bool _exhausted; 177 } 178 this(ubyte[] data) { 179 _data = data; 180 _size = data.length; 181 } 182 final override ulong getSize() { 183 return _size; 184 } 185 final override ubyte[] read() { 186 enforce( !_exhausted, "You can't read froum exhausted source" ); 187 size_t toRead = min(defaultBufferSize, _size - _offset); 188 auto result = _data[_offset.._offset+toRead]; 189 _offset += toRead; 190 if ( toRead == 0 ) { 191 _exhausted = true; 192 } 193 return result; 194 } 195 } 196 public class FormDataFile : FiniteReadable { 197 import std.file; 198 private { 199 File _fileHandle; 200 ulong _fileSize; 201 size_t _processed; 202 bool _exhausted; 203 } 204 this(File file) { 205 import std.file; 206 _fileHandle = file; 207 _fileSize = std.file.getSize(file.name); 208 } 209 final override ulong getSize() pure nothrow @safe { 210 return _fileSize; 211 } 212 final override ubyte[] read() { 213 enforce( !_exhausted, "You can't read froum exhausted source" ); 214 auto b = new ubyte[defaultBufferSize]; 215 auto r = _fileHandle.rawRead(b); 216 auto toRead = min(r.length, _fileSize - _processed); 217 if ( toRead == 0 ) { 218 _exhausted = true; 219 } 220 _processed += toRead; 221 return r[0..toRead]; 222 } 223 } 224 /// 225 /// This struct used to bulld POST's to forms. 226 /// Each part have name and data. data is something that can be read-ed and have size. 227 /// For example this can be string-like object (wrapped for reading) or opened File. 228 /// 229 public struct MultipartForm { 230 package struct FormData { 231 FiniteReadable input; 232 string name; 233 string[string] parameters; 234 this(string name, FiniteReadable i, string[string] parameters = null) { 235 this.input = i; 236 this.name = name; 237 this.parameters = parameters; 238 } 239 } 240 241 private FormData[] _sources; 242 auto add(FormData d) { 243 _sources ~= d; 244 return this; 245 } 246 auto add(string name, FiniteReadable i, string[string]parameters = null) { 247 _sources ~= FormData(name, i, parameters); 248 return this; 249 } 250 } 251 /// 252 253 /// 254 /// Request. 255 /// Configurable parameters: 256 /// $(B method) - string, method to use (GET, POST, ...) 257 /// $(B headers) - string[string], add any additional headers you'd like to send. 258 /// $(B authenticator) - class Auth, class to send auth headers. 259 /// $(B keepAlive) - bool, set true for keepAlive requests. default true. 260 /// $(B maxRedirects) - uint, maximum number of redirects. default 10. 261 /// $(B maxHeadersLength) - size_t, maximum length of server response headers. default = 32KB. 262 /// $(B maxContentLength) - size_t, maximun content length. delault - 0 = unlimited. 263 /// $(B bufferSize) - size_t, send and receive buffer size. default = 16KB. 264 /// $(B verbosity) - uint, level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0. 265 /// $(B proxy) - string, set proxy url if needed. default - null. 266 /// $(B cookie) - Tuple Cookie, Read/Write cookie You can get cookie setted by server, or set cookies before doing request. 267 /// $(B timeout) - Duration, Set timeout value for connect/receive/send. 268 /// 269 public struct HTTPRequest { 270 private { 271 enum _preHeaders = [ 272 "Accept-Encoding": "gzip, deflate", 273 "User-Agent": "dlang-requests" 274 ]; 275 string _method = "GET"; 276 URI _uri; 277 string[string] _headers; 278 string[] _filteredHeaders; 279 Auth _authenticator; 280 bool _keepAlive = true; 281 uint _maxRedirects = 10; 282 size_t _maxHeadersLength = 32 * 1024; // 32 KB 283 size_t _maxContentLength; // 0 - Unlimited 284 string _proxy; 285 uint _verbosity = 0; // 0 - no output, 1 - headers, 2 - headers+body info 286 Duration _timeout = 30.seconds; 287 size_t _bufferSize = defaultBufferSize; // 16k 288 bool _useStreaming; // return iterator instead of completed request 289 290 NetworkStream _stream; 291 HTTPResponse[] _history; // redirects history 292 DataPipe!ubyte _bodyDecoder; 293 DecodeChunked _unChunker; 294 long _contentLength; 295 long _contentReceived; 296 Cookie[] _cookie; 297 SSLOptions _sslOptions; 298 } 299 package HTTPResponse _response; 300 301 mixin(Getter_Setter!string ("method")); 302 mixin(Getter_Setter!bool ("keepAlive")); 303 mixin(Getter_Setter!size_t ("maxContentLength")); 304 mixin(Getter_Setter!size_t ("maxHeadersLength")); 305 mixin(Getter_Setter!size_t ("bufferSize")); 306 mixin(Getter_Setter!uint ("maxRedirects")); 307 mixin(Getter_Setter!uint ("verbosity")); 308 mixin(Getter_Setter!string ("proxy")); 309 mixin(Getter_Setter!Duration ("timeout")); 310 mixin(Setter!Auth ("authenticator")); 311 mixin(Getter_Setter!bool ("useStreaming")); 312 mixin(Getter!long ("contentLength")); 313 mixin(Getter!long ("contentReceived")); 314 mixin(Getter_Setter!SSLOptions ("sslOptions")); 315 316 @property void sslSetVerifyPeer(bool v) pure @safe nothrow @nogc { 317 _sslOptions.setVerifyPeer(v); 318 } 319 @property void sslSetKeyFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc { 320 _sslOptions.setKeyFile(p, t); 321 } 322 @property void sslSetCertFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc { 323 _sslOptions.setCertFile(p, t); 324 } 325 @property void sslSetCaCert(string path) pure @safe nothrow @nogc { 326 _sslOptions.setCaCert(path); 327 } 328 @property final void cookie(Cookie[] s) pure @safe @nogc nothrow { 329 _cookie = s; 330 } 331 332 @property final Cookie[] cookie() pure @safe @nogc nothrow { 333 return _cookie; 334 } 335 336 this(string uri) { 337 _uri = URI(uri); 338 } 339 ~this() { 340 if ( _stream && _stream.isConnected) { 341 _stream.close(); 342 } 343 _stream = null; 344 _headers = null; 345 _authenticator = null; 346 _history = null; 347 _bodyDecoder = null; 348 _unChunker = null; 349 } 350 351 @property void uri(in URI newURI) { 352 handleURLChange(_uri, newURI); 353 _uri = newURI; 354 } 355 /// Add headers to request 356 /// Params: 357 /// headers = headers to send. 358 void addHeaders(in string[string] headers) { 359 foreach(pair; headers.byKeyValue) { 360 _headers[pair.key] = pair.value; 361 } 362 } 363 /// Remove headers from request 364 /// Params: 365 /// headers = headers to remove. 366 void removeHeaders(in string[] headers) pure { 367 _filteredHeaders ~= headers; 368 } 369 /// 370 /// compose headers to send 371 /// 372 private string[string] requestHeaders() { 373 string[string] generatedHeaders = _preHeaders; 374 375 if ( _authenticator ) { 376 _authenticator. 377 authHeaders(_uri.host). 378 byKeyValue. 379 each!(pair => generatedHeaders[pair.key] = pair.value); 380 } 381 382 generatedHeaders["Connection"] = _keepAlive?"Keep-Alive":"Close"; 383 generatedHeaders["Host"] = _uri.host; 384 385 if ( _uri.scheme !in standard_ports || _uri.port != standard_ports[_uri.scheme] ) { 386 generatedHeaders["Host"] ~= ":%d".format(_uri.port); 387 } 388 389 _headers.byKey.each!(h => generatedHeaders[h] = _headers[h]); 390 391 if ( _cookie.length ) { 392 auto cs = _cookie. 393 filter!(c => _uri.path.pathMatches(c.path) && _uri.host.domainMatches(c.domain)). 394 map!(c => "%s=%s".format(c.attr, c.value)). 395 joiner(";"); 396 generatedHeaders["Cookie"] = "%s".format(cs); 397 } 398 399 _filteredHeaders.each!(h => generatedHeaders.remove(h)); 400 401 return generatedHeaders; 402 } 403 /// 404 /// Build request string. 405 /// Handle proxy and query parameters. 406 /// 407 private @property string requestString(QueryParam[] params = null) { 408 if ( _proxy ) { 409 return "%s %s HTTP/1.1\r\n".format(_method, _uri.uri); 410 } 411 auto query = _uri.query.dup; 412 if ( params ) { 413 query ~= params2query(params); 414 if ( query[0] != '?' ) { 415 query = "?" ~ query; 416 } 417 } 418 return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.path, query); 419 } 420 /// 421 /// encode parameters and build query part of the url 422 /// 423 private static string params2query(in QueryParam[] params) pure @safe { 424 return params. 425 map!(a => "%s=%s".format(a.key.urlEncoded, a.value.urlEncoded)). 426 join("&"); 427 } 428 // 429 package unittest { 430 assert(params2query(queryParams("a","b", "c", " d "))=="a=b&c=%20d%20"); 431 } 432 /// 433 /// Analyze received headers, take appropriate actions: 434 /// check content length, attach unchunk and uncompress 435 /// 436 private void analyzeHeaders(in string[string] headers) { 437 438 _contentLength = -1; 439 _unChunker = null; 440 auto contentLength = "content-length" in headers; 441 if ( contentLength ) { 442 try { 443 _contentLength = to!long(*contentLength); 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 == "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) { 479 string lastHeader; 480 auto buffer = cast(string)input; 481 482 foreach(line; buffer.split("\n").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 >= 3 ) { 491 _response.code = parsed[1].to!ushort; 492 } 493 continue; 494 } 495 if ( line[0] == ' ' || line[0] == '\t' ) { 496 // unfolding https://tools.ietf.org/html/rfc822#section-3.1 497 if ( auto stored = lastHeader in _response._responseHeaders) { 498 *stored ~= line; 499 } 500 continue; 501 } 502 auto parsed = line.findSplit(":"); 503 auto header = parsed[0].toLower; 504 auto value = parsed[2].strip; 505 506 if ( _verbosity >= 1 ) { 507 writefln("< %s: %s", header, value); 508 } 509 510 lastHeader = header; 511 debug (requests) tracef("Header %s = %s", header, value); 512 513 if ( header != "set-cookie" ) { 514 auto stored = _response.responseHeaders.get(header, null); 515 if ( stored ) { 516 value = stored ~ ", " ~ value; 517 } 518 _response._responseHeaders[header] = value; 519 continue; 520 } 521 _cookie ~= processCookie(value); 522 } 523 } 524 525 /// 526 /// Process Set-Cookie header from server response 527 /// 528 private Cookie[] processCookie(string value ) pure { 529 // cookie processing 530 // 531 // as we can't join several set-cookie lines in single line 532 // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com 533 // < 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 534 // 535 Cookie[] res; 536 string[string] kv; 537 auto fields = value.split(";").map!strip; 538 while(!fields.empty) { 539 auto s = fields.front.findSplit("="); 540 fields.popFront; 541 if ( s[1] != "=" ) { 542 continue; 543 } 544 auto k = s[0]; 545 auto v = s[2]; 546 switch(k.toLower()) { 547 case "domain": 548 k = "domain"; 549 break; 550 case "path": 551 k = "path"; 552 break; 553 case "expires": 554 continue; 555 default: 556 break; 557 } 558 kv[k] = v; 559 } 560 if ( "domain" !in kv ) { 561 kv["domain"] = _uri.host; 562 } 563 if ( "path" !in kv ) { 564 kv["path"] = _uri.path; 565 } 566 auto domain = kv["domain"]; kv.remove("domain"); 567 auto path = kv["path"]; kv.remove("path"); 568 foreach(pair; kv.byKeyValue) { 569 auto _attr = pair.key; 570 auto _value = pair.value; 571 auto cookie = Cookie(path, domain, _attr, _value); 572 res ~= cookie; 573 } 574 return res; 575 } 576 /// 577 /// Do we received \r\n\r\n? 578 /// 579 private bool headersHaveBeenReceived(in ubyte[] data, ref Buffer!ubyte buffer, out string separator) const @safe { 580 foreach(s; ["\r\n\r\n", "\n\n"]) { 581 if ( data.canFind(s) || buffer.canFind(s) ) { 582 separator = s; 583 return true; 584 } 585 } 586 return false; 587 } 588 589 private bool followRedirectResponse() { 590 if ( _history.length >= _maxRedirects ) { 591 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 592 } 593 auto location = "location" in _response.responseHeaders; 594 if ( !location ) { 595 return false; 596 } 597 _history ~= _response; 598 auto connection = "connection" in _response._responseHeaders; 599 if ( !connection || *connection == "close" ) { 600 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 601 _stream.close(); 602 } 603 URI oldURI = _uri; 604 URI newURI = oldURI; 605 try { 606 newURI = URI(*location); 607 } catch (UriException e) { 608 debug(requests) trace("Can't parse Location:, try relative uri"); 609 newURI.path = *location; 610 newURI.uri = newURI.recalc_uri; 611 } 612 handleURLChange(oldURI, newURI); 613 oldURI = _response.uri; 614 _uri = newURI; 615 _response = new HTTPResponse; 616 _response.uri = oldURI; 617 _response.finalURI = newURI; 618 return true; 619 } 620 /// 621 /// If uri changed so that we have to change host or port, then we have to close socket stream 622 /// 623 private void handleURLChange(in URI from, in URI to) { 624 if ( _stream !is null && _stream.isConnected && 625 ( from.scheme != to.scheme || from.host != to.host || from.port != to.port) ) { 626 debug tracef("Have to reopen stream, because of URI change"); 627 _stream.close(); 628 } 629 } 630 /// 631 /// if we have new uri, then we need to check if we have to reopen existent connection 632 /// 633 private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) { 634 if (url is null && _uri.uri == "" ) { 635 throw new RequestException("No url configured", file, line); 636 } 637 638 if ( url !is null ) { 639 URI newURI = URI(url); 640 handleURLChange(_uri, newURI); 641 _uri = newURI; 642 } 643 } 644 /// 645 /// Setup connection. Handle proxy and https case 646 /// 647 private void setupConnection() { 648 if ( !_stream || !_stream.isConnected ) { 649 debug(requests) tracef("Set up new connection"); 650 URI uri; 651 if ( _proxy ) { 652 // use proxy uri to connect 653 uri.uri_parse(_proxy); 654 } else { 655 // use original uri 656 uri = _uri; 657 } 658 final switch (uri.scheme) { 659 case "http": 660 _stream = new TCPStream().connect(uri.host, uri.port, _timeout); 661 break; 662 case "https": 663 _stream = new SSLStream(_sslOptions).connect(uri.host, uri.port, _timeout); 664 break; 665 } 666 } else { 667 debug(requests) tracef("Use old connection"); 668 } 669 } 670 /// 671 /// Request sent, now receive response. 672 /// Find headers, split on headers and body, continue to receive body 673 /// 674 private void receiveResponse() { 675 676 _stream.readTimeout = timeout; 677 scope(exit) { 678 if ( _stream && _stream.isOpen ) { 679 _stream.readTimeout = 0.seconds; 680 } 681 } 682 683 _bodyDecoder = new DataPipe!ubyte(); 684 scope(exit) { 685 if ( !_useStreaming ) { 686 _bodyDecoder = null; 687 _unChunker = null; 688 } 689 } 690 691 auto buffer = Buffer!ubyte(); 692 Buffer!ubyte partialBody; 693 ptrdiff_t read; 694 string separator; 695 696 while(true) { 697 698 auto b = new ubyte[_bufferSize]; 699 read = _stream.receive(b); 700 701 debug(requests) tracef("read: %d", read); 702 if ( read == 0 ) { 703 break; 704 } 705 auto data = b[0..read]; 706 buffer.putNoCopy(data); 707 if ( verbosity>=3 ) { 708 writeln(data.dump.join("\n")); 709 } 710 711 if ( buffer.length > maxHeadersLength ) { 712 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength)); 713 } 714 if ( headersHaveBeenReceived(data, buffer, separator) ) { 715 auto s = buffer.data.findSplit(separator); 716 auto ResponseHeaders = s[0]; 717 partialBody = Buffer!ubyte(s[2]); 718 _contentReceived += partialBody.length; 719 parseResponseHeaders(ResponseHeaders); 720 break; 721 } 722 } 723 724 analyzeHeaders(_response._responseHeaders); 725 726 _bodyDecoder.putNoCopy(partialBody.data); 727 728 if ( _verbosity >= 2 ) writefln("< %d bytes of body received", partialBody.length); 729 730 if ( _method == "HEAD" ) { 731 // HEAD response have ContentLength, but have no body 732 return; 733 } 734 735 while( true ) { 736 if ( _contentLength >= 0 && _contentReceived >= _contentLength ) { 737 debug(requests) trace("Body received."); 738 break; 739 } 740 if ( _unChunker && _unChunker.done ) { 741 break; 742 } 743 if ( _useStreaming && _response._responseBody.length && !redirectCodes.canFind(_response.code) ) { 744 debug(requests) trace("streaming requested"); 745 _response.receiveAsRange.activated = true; 746 _response.receiveAsRange.data = _response._responseBody.data; 747 _response.receiveAsRange.read = delegate ubyte[] () { 748 while(true) { 749 // check if we received everything we need 750 if ( ( _unChunker && _unChunker.done ) 751 || !_stream.isConnected() 752 || (_contentLength > 0 && _contentReceived >= _contentLength) ) 753 { 754 debug(requests) trace("streaming_in receive completed"); 755 _bodyDecoder.flush(); 756 return _bodyDecoder.get(); 757 } 758 // have to continue 759 auto b = new ubyte[_bufferSize]; 760 try { 761 read = _stream.receive(b); 762 } 763 catch (Exception e) { 764 throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e); 765 } 766 debug(requests) tracef("streaming_in received %d bytes", read); 767 768 if ( read == 0 ) { 769 debug(requests) tracef("streaming_in: server closed connection"); 770 _bodyDecoder.flush(); 771 return _bodyDecoder.get(); 772 } 773 774 if ( verbosity>=3 ) { 775 writeln(b[0..read].dump.join("\n")); 776 } 777 778 _contentReceived += read; 779 _bodyDecoder.putNoCopy(b[0..read]); 780 auto res = _bodyDecoder.getNoCopy(); 781 if ( res.length == 0 ) { 782 // there were nothing to produce (beginning of the chunk or no decompressed data) 783 continue; 784 } 785 if (res.length == 1) { 786 return res[0]; 787 } 788 // 789 // I'd like to "return _bodyDecoder.getNoCopy().join;" but if is slower 790 // 791 auto total = res.map!(b=>b.length).sum; 792 // create buffer for joined bytes 793 ubyte[] joined = new ubyte[total]; 794 size_t p; 795 // memcopy 796 foreach(ref _; res) { 797 joined[p .. p + _.length] = _; 798 p += _.length; 799 } 800 return joined; 801 } 802 assert(0); 803 }; 804 // we prepared for streaming 805 return; 806 } 807 808 auto b = new ubyte[_bufferSize]; 809 read = _stream.receive(b); 810 811 if ( read == 0 ) { 812 debug(requests) trace("read done"); 813 break; 814 } 815 if ( _verbosity >= 2 ) { 816 writefln("< %d bytes of body received", read); 817 } 818 819 if ( verbosity>=3 ) { 820 writeln(b[0..read].dump.join("\n")); 821 } 822 823 debug(requests) tracef("read: %d", read); 824 _contentReceived += read; 825 if ( _maxContentLength && _contentReceived > _maxContentLength ) { 826 throw new RequestException("ContentLength > maxContentLength (%d>%d)". 827 format(_contentLength, _maxContentLength)); 828 } 829 830 _bodyDecoder.putNoCopy(b[0..read]); // send buffer to all decoders 831 832 _bodyDecoder.getNoCopy. // fetch result and place to body 833 each!(b => _response._responseBody.putNoCopy(b)); 834 835 debug(requests) tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", _contentReceived, _contentLength, _response._responseBody.length); 836 837 } 838 _bodyDecoder.flush(); 839 _response._responseBody.putNoCopy(_bodyDecoder.get()); 840 } 841 private bool serverClosedKeepAliveConnection() pure @safe nothrow { 842 return _response._responseHeaders.length == 0 && _keepAlive; 843 } 844 private bool isIdempotent(in string method) pure @safe nothrow { 845 return ["GET", "HEAD"].canFind(method); 846 } 847 /// 848 /// Send multipart for request. 849 /// You would like to use this method for sending large portions of mixed data or uploading files to forms. 850 /// 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) 851 /// Params: 852 /// url = url 853 /// sources = array of sources. 854 HTTPResponse exec(string method="POST")(string url, MultipartForm sources) { 855 import std.uuid; 856 import std.file; 857 // 858 // application/json 859 // 860 bool restartedRequest = false; 861 862 _method = method; 863 864 _response = new HTTPResponse; 865 checkURL(url); 866 _response.uri = _uri; 867 _response.finalURI = _uri; 868 869 connect: 870 _contentReceived = 0; 871 _response._startedAt = Clock.currTime; 872 setupConnection(); 873 874 if ( !_stream.isConnected() ) { 875 return _response; 876 } 877 _response._connectedAt = Clock.currTime; 878 879 Appender!string req; 880 req.put(requestString()); 881 882 string boundary = randomUUID().toString; 883 string[] partHeaders; 884 size_t contentLength; 885 886 foreach(ref part; sources._sources) { 887 string h = "--" ~ boundary ~ "\r\n"; 888 string disposition = `form-data; name="%s"`.format(part.name); 889 string optionals = part. 890 parameters.byKeyValue(). 891 filter!(p => p.key!="Content-Type"). 892 map! (p => "%s=%s".format(p.key, p.value)). 893 join("; "); 894 895 h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n"; 896 897 auto contentType = "Content-Type" in part.parameters; 898 if ( contentType ) { 899 h ~= "Content-Type: " ~ *contentType ~ "\r\n"; 900 } 901 902 h ~= "\r\n"; 903 partHeaders ~= h; 904 contentLength += h.length + part.input.getSize() + "\r\n".length; 905 } 906 contentLength += "--".length + boundary.length + "--\r\n".length; 907 908 auto h = requestHeaders(); 909 h["Content-Type"] = "multipart/form-data; boundary=" ~ boundary; 910 h["Content-Length"] = to!string(contentLength); 911 h.byKeyValue. 912 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 913 each!(h => req.put(h)); 914 req.put("\r\n"); 915 916 debug(requests) trace(req.data); 917 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 918 919 try { 920 _stream.send(req.data()); 921 foreach(ref source; sources._sources) { 922 debug(requests) tracef("sending part headers <%s>", partHeaders.front); 923 _stream.send(partHeaders.front); 924 partHeaders.popFront; 925 while (true) { 926 auto chunk = source.input.read(); 927 if ( chunk.length <= 0 ) { 928 break; 929 } 930 _stream.send(chunk); 931 } 932 _stream.send("\r\n"); 933 } 934 _stream.send("--" ~ boundary ~ "--\r\n"); 935 _response._requestSentAt = Clock.currTime; 936 receiveResponse(); 937 _response._finishedAt = Clock.currTime; 938 } 939 catch (NetworkException e) { 940 errorf("Error sending request: ", e.msg); 941 return _response; 942 } 943 if ( _useStreaming ) { 944 if ( _response._receiveAsRange.activated ) { 945 debug(requests) trace("streaming_in activated"); 946 return _response; 947 } else { 948 _response._receiveAsRange.data = _response.responseBody.data; 949 } 950 } 951 auto connection = "connection" in _response._responseHeaders; 952 if ( !connection || *connection == "close" ) { 953 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 954 _stream.close(); 955 } 956 if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) { 957 if ( _method != "GET" ) { 958 return this.get(); 959 } 960 goto connect; 961 } 962 _response._history = _history; 963 /// 964 return _response; 965 } 966 /// 967 /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked). 968 /// 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. 969 /// 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. 970 /// 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. 971 /// 972 /// Parameters: 973 /// url = url 974 /// content = string or input range 975 /// contentType = content type 976 /// Returns: 977 /// Response 978 /// Examples: 979 /// --------------------------------------------------------------------------------------------------------- 980 /// rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 981 /// 982 /// auto s = lineSplitter("one,\ntwo,\nthree."); 983 /// rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 984 /// 985 /// auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 986 /// rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 987 /// 988 /// auto f = File("tests/test.txt", "rb"); 989 /// rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 990 /// -------------------------------------------------------------------------------------------------------- 991 HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="application/octet-stream") 992 if ( (rank!R == 1) 993 || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 994 || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte))) 995 ) { 996 // 997 // application/json 998 // 999 bool restartedRequest = false; 1000 1001 _method = method; 1002 1003 _response = new HTTPResponse; 1004 checkURL(url); 1005 _response.uri = _uri; 1006 _response.finalURI = _uri; 1007 1008 connect: 1009 _contentReceived = 0; 1010 _response._startedAt = Clock.currTime; 1011 setupConnection(); 1012 1013 if ( !_stream.isConnected() ) { 1014 return _response; 1015 } 1016 _response._connectedAt = Clock.currTime; 1017 1018 Appender!string req; 1019 req.put(requestString()); 1020 1021 auto h = requestHeaders; 1022 if ( contentType ) { 1023 h["Content-Type"] = contentType; 1024 } 1025 static if ( rank!R == 1 ) { 1026 h["Content-Length"] = to!string(content.length); 1027 } else { 1028 h["Transfer-Encoding"] = "chunked"; 1029 } 1030 h.byKeyValue. 1031 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1032 each!(h => req.put(h)); 1033 req.put("\r\n"); 1034 1035 debug(requests) trace(req.data); 1036 if ( _verbosity >= 1 ) { 1037 req.data.splitLines.each!(a => writeln("> " ~ a)); 1038 } 1039 1040 try { 1041 // send headers 1042 _stream.send(req.data()); 1043 // send body 1044 static if ( rank!R == 1) { 1045 _stream.send(content); 1046 } else { 1047 while ( !content.empty ) { 1048 auto chunk = content.front; 1049 auto chunkHeader = "%x\r\n".format(chunk.length); 1050 debug(requests) tracef("sending %s%s", chunkHeader, chunk); 1051 _stream.send(chunkHeader); 1052 _stream.send(chunk); 1053 _stream.send("\r\n"); 1054 content.popFront; 1055 } 1056 debug(requests) tracef("sent"); 1057 _stream.send("0\r\n\r\n"); 1058 } 1059 _response._requestSentAt = Clock.currTime; 1060 receiveResponse(); 1061 _response._finishedAt = Clock.currTime; 1062 } catch (NetworkException e) { 1063 _stream.close(); 1064 throw new RequestException("Network error during data exchange"); 1065 } 1066 1067 if ( _useStreaming ) { 1068 if ( _response._receiveAsRange.activated ) { 1069 debug(requests) trace("streaming_in activated"); 1070 return _response; 1071 } else { 1072 _response._receiveAsRange.data = _response.responseBody.data; 1073 } 1074 } 1075 auto connection = "connection" in _response._responseHeaders; 1076 if ( !connection || *connection == "close" ) { 1077 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 1078 _stream.close(); 1079 } 1080 if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) { 1081 if ( _method != "GET" ) { 1082 return this.get(); 1083 } 1084 goto connect; 1085 } 1086 /// 1087 _response._history = _history; 1088 return _response; 1089 } 1090 /// 1091 /// Send request with pameters. 1092 /// If used for POST or PUT requests then application/x-www-form-urlencoded used. 1093 /// Request parameters will be encoded into request string or placed in request body for POST/PUT 1094 /// requests. 1095 /// Parameters: 1096 /// url = url 1097 /// params = request parameters 1098 /// Returns: 1099 /// Response 1100 /// Examples: 1101 /// --------------------------------------------------------------------------------- 1102 /// rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]); 1103 /// --------------------------------------------------------------------------------- 1104 /// 1105 HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null) { 1106 1107 _method = method; 1108 _response = new HTTPResponse; 1109 _history.length = 0; 1110 bool restartedRequest = false; // True if this is restarted keepAlive request 1111 string encoded; 1112 1113 checkURL(url); 1114 _response.uri = _uri; 1115 _response.finalURI = _uri; 1116 1117 connect: 1118 _contentReceived = 0; 1119 _response._startedAt = Clock.currTime; 1120 setupConnection(); 1121 1122 if ( !_stream.isConnected() ) { 1123 return _response; 1124 } 1125 _response._connectedAt = Clock.currTime; 1126 1127 auto h = requestHeaders(); 1128 1129 Appender!string req; 1130 1131 if ( ["POST", "PUT"].canFind(_method) && params ) { 1132 encoded = params2query(params); 1133 h["Content-Type"] = "application/x-www-form-urlencoded"; 1134 h["Content-Length"] = to!string(encoded.length); 1135 req.put(requestString()); 1136 } else { 1137 req.put(requestString(params)); 1138 } 1139 1140 h.byKeyValue. 1141 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1142 each!(h => req.put(h)); 1143 req.put("\r\n"); 1144 if ( encoded ) { 1145 req.put(encoded); 1146 } 1147 1148 debug(requests) trace(req.data); 1149 1150 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 1151 // 1152 // Now send request and receive response 1153 // 1154 try { 1155 _stream.send(req.data()); 1156 _response._requestSentAt = Clock.currTime; 1157 receiveResponse(); 1158 _response._finishedAt = Clock.currTime; 1159 } 1160 catch (NetworkException e) { 1161 // On SEND this can means: 1162 // we started to send request to the server, but it closed connection because of keepalive timeout. 1163 // We have to restart request if possible. 1164 1165 // On RECEIVE - if we received something - then this exception is real and unexpected error. 1166 // If we didn't receive anything - we can restart request again as it can be 1167 if ( _response._responseHeaders.length != 0 ) { 1168 _stream.close(); 1169 throw new RequestException("Unexpected network error"); 1170 } 1171 } 1172 1173 if ( serverClosedKeepAliveConnection() 1174 && !restartedRequest 1175 && isIdempotent(_method) 1176 ) { 1177 /// 1178 /// We didn't receive any data (keepalive connectioin closed?) 1179 /// and we can restart this request. 1180 /// Go ahead. 1181 /// 1182 debug(requests) tracef("Server closed keepalive connection"); 1183 _stream.close(); 1184 restartedRequest = true; 1185 goto connect; 1186 } 1187 1188 if ( _useStreaming ) { 1189 if ( _response._receiveAsRange.activated ) { 1190 debug(requests) trace("streaming_in activated"); 1191 return _response; 1192 } else { 1193 // this can happen if whole response body received together with headers 1194 _response._receiveAsRange.data = _response.responseBody.data; 1195 } 1196 } 1197 1198 auto connection = "connection" in _response._responseHeaders; 1199 if ( !connection || *connection == "close" ) { 1200 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 1201 _stream.close(); 1202 } 1203 if ( _verbosity >= 1 ) { 1204 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1205 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1206 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1207 } 1208 if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) { 1209 if ( _method != "GET" ) { 1210 return this.get(); 1211 } 1212 goto connect; 1213 } 1214 /// 1215 _response._history = _history; 1216 return _response; 1217 } 1218 1219 /// WRAPPERS 1220 /// 1221 /// send file(s) using POST and multipart form. 1222 /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear. 1223 /// Parameters: 1224 /// url = url 1225 /// files = array of PostFile structures 1226 /// Returns: 1227 /// Response 1228 /// Each PostFile structure contain path to file, and optional field name and content type. 1229 /// If no field name provided, then basename of the file will be used. 1230 /// application/octet-stream is default when no content type provided. 1231 /// Example: 1232 /// --------------------------------------------------------------- 1233 /// PostFile[] files = [ 1234 /// {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 1235 /// {fileName:"tests/test.txt"} 1236 /// ]; 1237 /// rs = rq.exec!"POST"("http://httpbin.org/post", files); 1238 /// --------------------------------------------------------------- 1239 /// 1240 HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") { 1241 MultipartForm multipart; 1242 File[] toClose; 1243 foreach(ref f; files) { 1244 File file = File(f.fileName, "rb"); 1245 toClose ~= file; 1246 string fileName = f.fileName ? f.fileName : f.fieldName; 1247 string contentType = f.contentType ? f.contentType : "application/octetstream"; 1248 multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]); 1249 } 1250 auto res = exec!"POST"(url, multipart); 1251 toClose.each!"a.close"; 1252 return res; 1253 } 1254 /// 1255 /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names) 1256 /// Consider switch to exec(url, QueryParams) as it more generic and clear. 1257 /// Parameters: 1258 /// url = url 1259 /// params = dictionary with field names as keys and field values as values. 1260 /// Returns: 1261 /// Response 1262 HTTPResponse exec(string method="GET")(string url, string[string] params) { 1263 return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array); 1264 } 1265 /// 1266 /// GET request. Simple wrapper over exec!"GET" 1267 /// Params: 1268 /// args = request parameters. see exec docs. 1269 /// 1270 HTTPResponse get(A...)(A args) { 1271 return exec!"GET"(args); 1272 } 1273 /// 1274 /// POST request. Simple wrapper over exec!"POST" 1275 /// Params: 1276 /// uri = endpoint uri 1277 /// args = request parameters. see exec docs. 1278 /// 1279 HTTPResponse post(A...)(string uri, A args) { 1280 return exec!"POST"(uri, args); 1281 } 1282 } 1283 1284 version(vibeD) { 1285 import std.json; 1286 package string httpTestServer() { 1287 return "http://httpbin.org/"; 1288 } 1289 package string fromJsonArrayToStr(JSONValue v) { 1290 return v.str; 1291 } 1292 } 1293 else { 1294 import std.json; 1295 package string httpTestServer() { 1296 return "http://127.0.0.1:8081/"; 1297 } 1298 package string fromJsonArrayToStr(JSONValue v) { 1299 return cast(string)(v.array.map!"cast(ubyte)a.integer".array); 1300 } 1301 } 1302 1303 1304 package unittest { 1305 import std.json; 1306 import std.array; 1307 1308 globalLogLevel(LogLevel.info); 1309 1310 string httpbinUrl = httpTestServer(); 1311 version(vibeD) { 1312 } 1313 else { 1314 import httpbin; 1315 auto server = httpbinApp(); 1316 server.start(); 1317 scope(exit) { 1318 server.stop(); 1319 } 1320 } 1321 HTTPRequest rq; 1322 HTTPResponse rs; 1323 info("Check GET"); 1324 rs = rq.get(httpbinUrl); 1325 assert(rs.code==200); 1326 assert(rs.responseBody.length > 0); 1327 1328 info("Check GET with AA params"); 1329 { 1330 rs = HTTPRequest().get(httpbinUrl ~ "get", ["c":" d", "a":"b"]); 1331 assert(rs.code == 200); 1332 auto json = parseJSON(rs.responseBody.data).object["args"].object; 1333 assert(json["c"].str == " d"); 1334 assert(json["a"].str == "b"); 1335 } 1336 info("Check POST files"); 1337 { 1338 import std.file; 1339 import std.path; 1340 auto tmpd = tempDir(); 1341 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1342 auto f = File(tmpfname, "wb"); 1343 f.rawWrite("abcdefgh\n12345678\n"); 1344 f.close(); 1345 // files 1346 PostFile[] files = [ 1347 {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 1348 {fileName: tmpfname} 1349 ]; 1350 rs = rq.post(httpbinUrl ~ "post", files); 1351 assert(rs.code==200); 1352 } 1353 info("Check POST chunked from file.byChunk"); 1354 { 1355 import std.file; 1356 import std.path; 1357 auto tmpd = tempDir(); 1358 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1359 auto f = File(tmpfname, "wb"); 1360 f.rawWrite("abcdefgh\n12345678\n"); 1361 f.close(); 1362 f = File(tmpfname, "rb"); 1363 rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream"); 1364 assert(rs.code==200); 1365 auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]); 1366 assert(data=="abcdefgh\n12345678\n"); 1367 f.close(); 1368 } 1369 info("Check POST chunked from lineSplitter"); 1370 { 1371 auto s = lineSplitter("one,\ntwo,\nthree."); 1372 rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream"); 1373 assert(rs.code==200); 1374 auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]); 1375 assert(data=="one,two,three."); 1376 } 1377 info("Check POST chunked from array"); 1378 { 1379 auto s = ["one,", "two,", "three."]; 1380 rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream"); 1381 assert(rs.code==200); 1382 auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]); 1383 assert(data=="one,two,three."); 1384 } 1385 info("Check POST chunked using std.range.chunks()"); 1386 { 1387 auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1388 rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream"); 1389 assert(rs.code==200); 1390 auto data = fromJsonArrayToStr(parseJSON(rs.responseBody.data).object["data"]); 1391 assert(data==s); 1392 } 1393 info("Check POST from QueryParams"); 1394 { 1395 rs = rq.post(httpbinUrl ~ "post", queryParams("name[]", "first", "name[]", 2)); 1396 assert(rs.code==200); 1397 auto data = parseJSON(rs.responseBody).object["form"].object; 1398 string[] a; 1399 try { 1400 a = to!(string[])(data["name[]"].str); 1401 } 1402 catch (JSONException e) { 1403 a = data["name[]"].array.map!"a.str".array; 1404 } 1405 assert(equal(["first", "2"], a)); 1406 } 1407 info("Check POST from AA"); 1408 { 1409 rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]); 1410 assert(rs.code==200); 1411 auto form = parseJSON(rs.responseBody.data).object["form"].object; 1412 assert(form["a"].str == "b "); 1413 assert(form["c"].str == "d"); 1414 } 1415 info("Check POST json"); 1416 { 1417 rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"a b", "c":[1,2,3]}`, "application/json"); 1418 assert(rs.code==200); 1419 auto json = parseJSON(rs.responseBody).object["args"].object; 1420 assert(json["b"].str == "x"); 1421 json = parseJSON(rs.responseBody).object["json"].object; 1422 assert(json["a"].str == "a b"); 1423 assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]); 1424 } 1425 info("Check HEAD"); 1426 rs = rq.exec!"HEAD"(httpbinUrl); 1427 assert(rs.code==200); 1428 info("Check DELETE"); 1429 rs = rq.exec!"DELETE"(httpbinUrl ~ "delete"); 1430 assert(rs.code==200); 1431 info("Check PUT"); 1432 rs = rq.exec!"PUT"(httpbinUrl ~ "put", `{"a":"b", "c":[1,2,3]}`, "application/json"); 1433 assert(rs.code==200); 1434 assert(parseJSON(rs.responseBody).object["json"].object["a"].str=="b"); 1435 info("Check PATCH"); 1436 rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream"); 1437 assert(rs.code==200); 1438 info("Check compressed content"); 1439 rs = rq.get(httpbinUrl ~ "gzip"); 1440 assert(rs.code==200); 1441 bool gzipped = parseJSON(rs.responseBody).object["gzipped"].type == JSON_TYPE.TRUE; 1442 assert(gzipped); 1443 info("gzip - ok"); 1444 rs = rq.get(httpbinUrl ~ "deflate"); 1445 assert(rs.code==200); 1446 bool deflated = parseJSON(rs.responseBody).object["deflated"].type == JSON_TYPE.TRUE; 1447 assert(deflated); 1448 info("deflate - ok"); 1449 1450 info("Check redirects"); 1451 rs = rq.get(httpbinUrl ~ "relative-redirect/2"); 1452 assert(rs.history.length == 2); 1453 assert(rs.code==200); 1454 rs = rq.get(httpbinUrl ~ "absolute-redirect/2"); 1455 assert(rs.history.length == 2); 1456 assert(rs.code==200); 1457 1458 rq.maxRedirects = 2; 1459 assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3")); 1460 1461 info("Check cookie"); 1462 { 1463 rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef"); 1464 assert(rs.code == 200); 1465 auto json = parseJSON(rs.responseBody.data).object["cookies"].object; 1466 assert(json["A"].str == "abcd"); 1467 assert(json["b"].str == "cdef"); 1468 foreach(c; rq.cookie) { 1469 final switch(c.attr) { 1470 case "A": 1471 assert(c.value == "abcd"); 1472 break; 1473 case "b": 1474 assert(c.value == "cdef"); 1475 break; 1476 } 1477 } 1478 } 1479 info("Check chunked content"); 1480 rs = rq.get(httpbinUrl ~ "range/1024"); 1481 assert(rs.code==200); 1482 assert(rs.responseBody.length==1024); 1483 1484 info("Check basic auth"); 1485 rq.authenticator = new BasicAuthentication("user", "passwd"); 1486 rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd"); 1487 assert(rs.code==200); 1488 1489 info("Check limits"); 1490 rq = HTTPRequest(); 1491 rq.maxContentLength = 1; 1492 assertThrown!RequestException(rq.get(httpbinUrl)); 1493 rq = HTTPRequest(); 1494 rq.maxHeadersLength = 1; 1495 assertThrown!RequestException(rq.get(httpbinUrl)); 1496 rq = HTTPRequest(); 1497 info("Check POST multiPartForm"); 1498 { 1499 /// This is example on usage files with MultipartForm data. 1500 /// For this example we have to create files which will be sent. 1501 import std.file; 1502 import std.path; 1503 /// preapare files 1504 auto tmpd = tempDir(); 1505 auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt"; 1506 auto f = File(tmpfname1, "wb"); 1507 f.rawWrite("file1 content\n"); 1508 f.close(); 1509 auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt"; 1510 f = File(tmpfname2, "wb"); 1511 f.rawWrite("file2 content\n"); 1512 f.close(); 1513 /// 1514 /// Ok, files ready. 1515 /// Now we will prepare Form data 1516 /// 1517 File f1 = File(tmpfname1, "rb"); 1518 File f2 = File(tmpfname2, "rb"); 1519 scope(exit) { 1520 f1.close(); 1521 f2.close(); 1522 } 1523 /// 1524 /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type 1525 /// 1526 MultipartForm mForm = MultipartForm(). 1527 add(formData("Field1", cast(ubyte[])"form field from memory")). 1528 add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])). 1529 add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])). 1530 add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"])); 1531 /// everything ready, send request 1532 rs = rq.post(httpbinUrl ~ "post", mForm); 1533 } 1534 info("Check exception handling, error messages and timeous are OK"); 1535 rq.timeout = 1.seconds; 1536 assertThrown!TimeoutException(rq.get(httpbinUrl ~ "delay/3")); 1537 // assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/")); 1538 // assertThrown!ConnectError(rq.get("http://1.1.1.1/")); 1539 // assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/")); 1540 }