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