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