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, 307, 308]; 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 if ( "Host" !in generatedHeaders ) { 486 generatedHeaders["Host"] = _uri.host; 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 >= 2 ) { 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" && _response.code != 307 && _response.code != 308 ) { 1117 // 307 and 308 do not change method 1118 return this.get(); 1119 } 1120 goto connect; 1121 } 1122 _response._history = _history; 1123 /// 1124 return _response; 1125 } 1126 1127 // we use this if we send from ubyte[][] and user provided Content-Length 1128 private void sendFlattenContent(T)(NetworkStream _stream, T content) { 1129 while ( !content.empty ) { 1130 auto chunk = content.front; 1131 _stream.send(chunk); 1132 content.popFront; 1133 } 1134 debug(requests) tracef("sent"); 1135 } 1136 // we use this if we send from ubyte[][] as chunked content 1137 private void sendChunkedContent(T)(NetworkStream _stream, T content) { 1138 while ( !content.empty ) { 1139 auto chunk = content.front; 1140 auto chunkHeader = "%x\r\n".format(chunk.length); 1141 debug(requests) tracef("sending %s%s", chunkHeader, chunk); 1142 _stream.send(chunkHeader); 1143 _stream.send(chunk); 1144 _stream.send("\r\n"); 1145 content.popFront; 1146 } 1147 debug(requests) tracef("sent"); 1148 _stream.send("0\r\n\r\n"); 1149 } 1150 /// 1151 /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked). 1152 /// 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. 1153 /// 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. 1154 /// 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. 1155 /// 1156 /// Parameters: 1157 /// url = url 1158 /// content = string or input range 1159 /// contentType = content type 1160 /// Returns: 1161 /// Response 1162 /// Examples: 1163 /// --------------------------------------------------------------------------------------------------------- 1164 /// rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 1165 /// 1166 /// auto s = lineSplitter("one,\ntwo,\nthree."); 1167 /// rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 1168 /// 1169 /// auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1170 /// rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 1171 /// 1172 /// auto f = File("tests/test.txt", "rb"); 1173 /// rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 1174 /// -------------------------------------------------------------------------------------------------------- 1175 HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="application/octet-stream") 1176 if ( (rank!R == 1) 1177 || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 1178 || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte))) 1179 ) { 1180 if ( _response && _response._receiveAsRange.activated && _stream && _stream.isConnected ) { 1181 _stream.close(); 1182 } 1183 // 1184 // application/json 1185 // 1186 bool restartedRequest = false; 1187 bool send_flat; 1188 1189 _method = method; 1190 1191 _response = new HTTPResponse; 1192 checkURL(url); 1193 _response.uri = _uri; 1194 _response.finalURI = _uri; 1195 1196 connect: 1197 _contentReceived = 0; 1198 _response._startedAt = Clock.currTime; 1199 setupConnection(); 1200 1201 if ( !_stream.isConnected() ) { 1202 return _response; 1203 } 1204 _response._connectedAt = Clock.currTime; 1205 1206 Appender!string req; 1207 req.put(requestString()); 1208 1209 auto h = requestHeaders; 1210 if ( contentType && "Content-Type" !in h ) { 1211 h["Content-Type"] = contentType; 1212 } 1213 static if ( rank!R == 1 ) { 1214 h["Content-Length"] = to!string(content.length); 1215 } else { 1216 if ("Content-Length" in _headers ) { 1217 debug(requests) tracef("User provided content-length for chunked content"); 1218 send_flat = true; 1219 } else { 1220 h["Transfer-Encoding"] = "chunked"; 1221 send_flat = false; 1222 } 1223 } 1224 h.byKeyValue. 1225 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1226 each!(h => req.put(h)); 1227 req.put("\r\n"); 1228 1229 debug(requests) trace(req.data); 1230 if ( _verbosity >= 1 ) { 1231 req.data.splitLines.each!(a => writeln("> " ~ a)); 1232 } 1233 1234 try { 1235 // send headers 1236 _stream.send(req.data()); 1237 // send body 1238 static if ( rank!R == 1) { 1239 _stream.send(content); 1240 } else { 1241 if ( send_flat ) { 1242 sendFlattenContent(_stream, content); 1243 } else { 1244 sendChunkedContent(_stream, content); 1245 } 1246 } 1247 _response._requestSentAt = Clock.currTime; 1248 receiveResponse(); 1249 _response._finishedAt = Clock.currTime; 1250 } catch (NetworkException e) { 1251 _stream.close(); 1252 throw new RequestException("Network error during data exchange"); 1253 } 1254 1255 if ( _useStreaming ) { 1256 if ( _response._receiveAsRange.activated ) { 1257 debug(requests) trace("streaming_in activated"); 1258 return _response; 1259 } else { 1260 _response._receiveAsRange.data = _response.responseBody.data; 1261 } 1262 } 1263 auto connection = "connection" in _response._responseHeaders; 1264 if ( !connection || *connection == "close" ) { 1265 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 1266 _stream.close(); 1267 } 1268 if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) { 1269 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1270 // 307 and 308 do not change method 1271 return this.get(); 1272 } 1273 goto connect; 1274 } 1275 /// 1276 _response._history = _history; 1277 return _response; 1278 } 1279 /// 1280 /// Send request with pameters. 1281 /// If used for POST or PUT requests then application/x-www-form-urlencoded used. 1282 /// Request parameters will be encoded into request string or placed in request body for POST/PUT 1283 /// requests. 1284 /// Parameters: 1285 /// url = url 1286 /// params = request parameters 1287 /// Returns: 1288 /// Response 1289 /// Examples: 1290 /// --------------------------------------------------------------------------------- 1291 /// rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]); 1292 /// --------------------------------------------------------------------------------- 1293 /// 1294 HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null) { 1295 1296 if ( _response && _response._receiveAsRange.activated && _stream && _stream.isConnected ) { 1297 _stream.close(); 1298 } 1299 _method = method; 1300 _response = new HTTPResponse; 1301 _history.length = 0; 1302 bool restartedRequest = false; // True if this is restarted keepAlive request 1303 string encoded; 1304 1305 checkURL(url); 1306 _response.uri = _uri; 1307 _response.finalURI = _uri; 1308 1309 connect: 1310 _contentReceived = 0; 1311 _response._startedAt = Clock.currTime; 1312 setupConnection(); 1313 1314 if ( !_stream.isConnected() ) { 1315 return _response; 1316 } 1317 _response._connectedAt = Clock.currTime; 1318 1319 auto h = requestHeaders(); 1320 1321 Appender!string req; 1322 1323 switch (_method) { 1324 case "POST","PUT": 1325 encoded = params2query(params); 1326 if ( "Content-Type" !in h ) { 1327 h["Content-Type"] = "application/x-www-form-urlencoded"; 1328 } 1329 if ( "Content-Length" !in h && encoded.length > 0) { 1330 h["Content-Length"] = to!string(encoded.length); 1331 } 1332 req.put(requestString()); 1333 break; 1334 default: 1335 req.put(requestString(params)); 1336 } 1337 1338 h.byKeyValue. 1339 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1340 each!(h => req.put(h)); 1341 req.put("\r\n"); 1342 if ( encoded ) { 1343 req.put(encoded); 1344 } 1345 1346 debug(requests) trace(req.data); 1347 1348 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 1349 // 1350 // Now send request and receive response 1351 // 1352 try { 1353 _stream.send(req.data()); 1354 _response._requestSentAt = Clock.currTime; 1355 receiveResponse(); 1356 _response._finishedAt = Clock.currTime; 1357 } 1358 catch (NetworkException e) { 1359 // On SEND this can means: 1360 // we started to send request to the server, but it closed connection because of keepalive timeout. 1361 // We have to restart request if possible. 1362 1363 // On RECEIVE - if we received something - then this exception is real and unexpected error. 1364 // If we didn't receive anything - we can restart request again as it can be 1365 if ( _response._responseHeaders.length != 0 ) { 1366 _stream.close(); 1367 throw new RequestException("Unexpected network error"); 1368 } 1369 } 1370 1371 if ( serverClosedKeepAliveConnection() 1372 && !restartedRequest 1373 && isIdempotent(_method) 1374 ) { 1375 /// 1376 /// We didn't receive any data (keepalive connectioin closed?) 1377 /// and we can restart this request. 1378 /// Go ahead. 1379 /// 1380 debug(requests) tracef("Server closed keepalive connection"); 1381 _stream.close(); 1382 restartedRequest = true; 1383 goto connect; 1384 } 1385 1386 if ( _useStreaming ) { 1387 if ( _response._receiveAsRange.activated ) { 1388 debug(requests) trace("streaming_in activated"); 1389 return _response; 1390 } else { 1391 // this can happen if whole response body received together with headers 1392 _response._receiveAsRange.data = _response.responseBody.data; 1393 } 1394 } 1395 1396 auto connection = "connection" in _response._responseHeaders; 1397 if ( !connection || *connection == "close" ) { 1398 debug(requests) tracef("Closing connection because of 'Connection: close' or no 'Connection' header"); 1399 _stream.close(); 1400 } 1401 if ( _verbosity >= 1 ) { 1402 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1403 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1404 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1405 } 1406 if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) { 1407 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1408 // 307 and 308 do not change method 1409 return this.get(); 1410 } 1411 goto connect; 1412 } 1413 /// 1414 _response._history = _history; 1415 return _response; 1416 } 1417 1418 /// WRAPPERS 1419 /// 1420 /// send file(s) using POST and multipart form. 1421 /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear. 1422 /// Parameters: 1423 /// url = url 1424 /// files = array of PostFile structures 1425 /// Returns: 1426 /// Response 1427 /// Each PostFile structure contain path to file, and optional field name and content type. 1428 /// If no field name provided, then basename of the file will be used. 1429 /// application/octet-stream is default when no content type provided. 1430 /// Example: 1431 /// --------------------------------------------------------------- 1432 /// PostFile[] files = [ 1433 /// {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 1434 /// {fileName:"tests/test.txt"} 1435 /// ]; 1436 /// rs = rq.exec!"POST"("http://httpbin.org/post", files); 1437 /// --------------------------------------------------------------- 1438 /// 1439 HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") { 1440 MultipartForm multipart; 1441 File[] toClose; 1442 foreach(ref f; files) { 1443 File file = File(f.fileName, "rb"); 1444 toClose ~= file; 1445 string fileName = f.fileName ? f.fileName : f.fieldName; 1446 string contentType = f.contentType ? f.contentType : "application/octetstream"; 1447 multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]); 1448 } 1449 auto res = exec!"POST"(url, multipart); 1450 toClose.each!"a.close"; 1451 return res; 1452 } 1453 /// 1454 /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names) 1455 /// Consider switch to exec(url, QueryParams) as it more generic and clear. 1456 /// Parameters: 1457 /// url = url 1458 /// params = dictionary with field names as keys and field values as values. 1459 /// Returns: 1460 /// Response 1461 HTTPResponse exec(string method="GET")(string url, string[string] params) { 1462 return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array); 1463 } 1464 /// 1465 /// GET request. Simple wrapper over exec!"GET" 1466 /// Params: 1467 /// args = request parameters. see exec docs. 1468 /// 1469 HTTPResponse get(A...)(A args) { 1470 return exec!"GET"(args); 1471 } 1472 /// 1473 /// POST request. Simple wrapper over exec!"POST" 1474 /// Params: 1475 /// uri = endpoint uri 1476 /// args = request parameters. see exec docs. 1477 /// 1478 HTTPResponse post(A...)(string uri, A args) { 1479 return exec!"POST"(uri, args); 1480 } 1481 } 1482 1483 version(vibeD) { 1484 import std.json; 1485 package string httpTestServer() { 1486 return "http://httpbin.org/"; 1487 } 1488 package string fromJsonArrayToStr(JSONValue v) { 1489 return v.str; 1490 } 1491 } 1492 else { 1493 import std.json; 1494 package string httpTestServer() { 1495 return "http://127.0.0.1:8081/"; 1496 } 1497 package string fromJsonArrayToStr(JSONValue v) { 1498 return cast(string)(v.array.map!"cast(ubyte)a.integer".array); 1499 } 1500 } 1501 1502 1503 package unittest { 1504 import std.json; 1505 import std.array; 1506 1507 globalLogLevel(LogLevel.info); 1508 1509 string httpbinUrl = httpTestServer(); 1510 version(vibeD) { 1511 } 1512 else { 1513 import httpbin; 1514 auto server = httpbinApp(); 1515 server.start(); 1516 scope(exit) { 1517 server.stop(); 1518 } 1519 } 1520 HTTPRequest rq; 1521 HTTPResponse rs; 1522 info("Check GET"); 1523 URI uri = URI(httpbinUrl); 1524 rs = rq.get(httpbinUrl); 1525 assert(rs.code==200); 1526 assert(rs.responseBody.length > 0); 1527 assert(rq.format("%m|%h|%p|%P|%q|%U") == 1528 "GET|%s|%d|%s||%s" 1529 .format(uri.host, uri.port, uri.path, httpbinUrl)); 1530 info("Check GET with AA params"); 1531 { 1532 rs = HTTPRequest().get(httpbinUrl ~ "get", ["c":" d", "a":"b"]); 1533 assert(rs.code == 200); 1534 auto json = parseJSON(cast(string)rs.responseBody.data).object["args"].object; 1535 assert(json["c"].str == " d"); 1536 assert(json["a"].str == "b"); 1537 } 1538 info("Check POST files"); 1539 { 1540 import std.file; 1541 import std.path; 1542 auto tmpd = tempDir(); 1543 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1544 auto f = File(tmpfname, "wb"); 1545 f.rawWrite("abcdefgh\n12345678\n"); 1546 f.close(); 1547 // files 1548 PostFile[] files = [ 1549 {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 1550 {fileName: tmpfname} 1551 ]; 1552 rs = rq.post(httpbinUrl ~ "post", files); 1553 assert(rs.code==200); 1554 } 1555 info("Check POST chunked from file.byChunk"); 1556 { 1557 import std.file; 1558 import std.path; 1559 auto tmpd = tempDir(); 1560 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1561 auto f = File(tmpfname, "wb"); 1562 f.rawWrite("abcdefgh\n12345678\n"); 1563 f.close(); 1564 f = File(tmpfname, "rb"); 1565 rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream"); 1566 if (httpbinUrl != "http://httpbin.org/") { 1567 assert(rs.code==200); 1568 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1569 assert(data=="abcdefgh\n12345678\n"); 1570 } 1571 f.close(); 1572 } 1573 info("Check POST chunked from lineSplitter"); 1574 { 1575 auto s = lineSplitter("one,\ntwo,\nthree."); 1576 rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream"); 1577 if (httpbinUrl != "http://httpbin.org/") { 1578 assert(rs.code==200); 1579 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1580 assert(data=="one,two,three."); 1581 } 1582 } 1583 info("Check POST chunked from array"); 1584 { 1585 auto s = ["one,", "two,", "three."]; 1586 rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream"); 1587 if (httpbinUrl != "http://httpbin.org/") { 1588 assert(rs.code==200); 1589 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1590 assert(data=="one,two,three."); 1591 } 1592 } 1593 info("Check POST chunked using std.range.chunks()"); 1594 { 1595 auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1596 rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream"); 1597 if (httpbinUrl != "http://httpbin.org/") { 1598 assert(rs.code==200); 1599 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody.data).object["data"]); 1600 assert(data==s); 1601 } 1602 } 1603 info("Check POST from QueryParams"); 1604 { 1605 rs = rq.post(httpbinUrl ~ "post", queryParams("name[]", "first", "name[]", 2)); 1606 assert(rs.code==200); 1607 auto data = parseJSON(cast(string)rs.responseBody).object["form"].object; 1608 string[] a; 1609 try { 1610 a = to!(string[])(data["name[]"].str); 1611 } 1612 catch (JSONException e) { 1613 a = data["name[]"].array.map!"a.str".array; 1614 } 1615 assert(equal(["first", "2"], a)); 1616 } 1617 info("Check POST from AA"); 1618 { 1619 rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]); 1620 assert(rs.code==200); 1621 auto form = parseJSON(cast(string)rs.responseBody.data).object["form"].object; 1622 assert(form["a"].str == "b "); 1623 assert(form["c"].str == "d"); 1624 } 1625 info("Check POST json"); 1626 { 1627 rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"a b", "c":[1,2,3]}`, "application/json"); 1628 assert(rs.code==200); 1629 auto json = parseJSON(cast(string)rs.responseBody).object["args"].object; 1630 assert(json["b"].str == "x"); 1631 json = parseJSON(cast(string)rs.responseBody).object["json"].object; 1632 assert(json["a"].str == "a b"); 1633 assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]); 1634 } 1635 info("Check HEAD"); 1636 rs = rq.exec!"HEAD"(httpbinUrl); 1637 assert(rs.code==200); 1638 info("Check DELETE"); 1639 rs = rq.exec!"DELETE"(httpbinUrl ~ "delete"); 1640 assert(rs.code==200); 1641 info("Check PUT"); 1642 rs = rq.exec!"PUT"(httpbinUrl ~ "put", `{"a":"b", "c":[1,2,3]}`, "application/json"); 1643 assert(rs.code==200); 1644 assert(parseJSON(cast(string)rs.responseBody).object["json"].object["a"].str=="b"); 1645 info("Check PATCH"); 1646 rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream"); 1647 assert(rs.code==200); 1648 info("Check compressed content"); 1649 rs = rq.get(httpbinUrl ~ "gzip"); 1650 assert(rs.code==200); 1651 bool gzipped = parseJSON(cast(string)rs.responseBody).object["gzipped"].type == JSON_TYPE.TRUE; 1652 assert(gzipped); 1653 info("gzip - ok"); 1654 rs = rq.get(httpbinUrl ~ "deflate"); 1655 assert(rs.code==200); 1656 bool deflated = parseJSON(cast(string)rs.responseBody).object["deflated"].type == JSON_TYPE.TRUE; 1657 assert(deflated); 1658 info("deflate - ok"); 1659 1660 info("Check redirects"); 1661 rs = rq.get(httpbinUrl ~ "relative-redirect/2"); 1662 assert(rs.history.length == 2); 1663 assert(rs.code==200); 1664 rs = rq.get(httpbinUrl ~ "absolute-redirect/2"); 1665 assert(rs.history.length == 2); 1666 assert(rs.code==200); 1667 1668 rq.maxRedirects = 2; 1669 assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3")); 1670 1671 info("Check cookie"); 1672 { 1673 rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef"); 1674 assert(rs.code == 200); 1675 auto json = parseJSON(cast(string)rs.responseBody.data).object["cookies"].object; 1676 assert(json["A"].str == "abcd"); 1677 assert(json["b"].str == "cdef"); 1678 foreach(c; rq.cookie) { 1679 final switch(c.attr) { 1680 case "A": 1681 assert(c.value == "abcd"); 1682 break; 1683 case "b": 1684 assert(c.value == "cdef"); 1685 break; 1686 } 1687 } 1688 } 1689 info("Check chunked content"); 1690 rs = rq.get(httpbinUrl ~ "range/1024"); 1691 assert(rs.code==200); 1692 assert(rs.responseBody.length==1024); 1693 1694 info("Check basic auth"); 1695 rq.authenticator = new BasicAuthentication("user", "passwd"); 1696 rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd"); 1697 assert(rs.code==200); 1698 1699 info("Check limits"); 1700 rq = HTTPRequest(); 1701 rq.maxContentLength = 1; 1702 assertThrown!RequestException(rq.get(httpbinUrl)); 1703 rq = HTTPRequest(); 1704 rq.maxHeadersLength = 1; 1705 assertThrown!RequestException(rq.get(httpbinUrl)); 1706 rq = HTTPRequest(); 1707 info("Check POST multiPartForm"); 1708 { 1709 /// This is example on usage files with MultipartForm data. 1710 /// For this example we have to create files which will be sent. 1711 import std.file; 1712 import std.path; 1713 /// preapare files 1714 auto tmpd = tempDir(); 1715 auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt"; 1716 auto f = File(tmpfname1, "wb"); 1717 f.rawWrite("file1 content\n"); 1718 f.close(); 1719 auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt"; 1720 f = File(tmpfname2, "wb"); 1721 f.rawWrite("file2 content\n"); 1722 f.close(); 1723 /// 1724 /// Ok, files ready. 1725 /// Now we will prepare Form data 1726 /// 1727 File f1 = File(tmpfname1, "rb"); 1728 File f2 = File(tmpfname2, "rb"); 1729 scope(exit) { 1730 f1.close(); 1731 f2.close(); 1732 } 1733 /// 1734 /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type 1735 /// 1736 MultipartForm mForm = MultipartForm(). 1737 add(formData("Field1", cast(ubyte[])"form field from memory")). 1738 add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])). 1739 add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])). 1740 add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"])); 1741 /// everything ready, send request 1742 rs = rq.post(httpbinUrl ~ "post", mForm); 1743 } 1744 info("Check exception handling, error messages and timeous are OK"); 1745 rq.timeout = 1.seconds; 1746 assertThrown!TimeoutException(rq.get(httpbinUrl ~ "delay/3")); 1747 // assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/")); 1748 // assertThrown!ConnectError(rq.get("http://1.1.1.1/")); 1749 // assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/")); 1750 }