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