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