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