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