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 }