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 }