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