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