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