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.socket;
13 import std.string;
14 import std.traits;
15 import std.typecons;
16 import std.experimental.logger;
17 import core.thread;
18 import core.stdc.errno;
19 
20 import requests.streams;
21 import requests.uri;
22 import requests.utils;
23 import requests.base;
24 
25 static this() {
26     globalLogLevel(LogLevel.error);
27 }
28 
29 static immutable ushort[] redirectCodes = [301, 302, 303];
30 
31 static string urlEncoded(string p) pure @safe {
32     immutable string[dchar] translationTable = [
33         ' ':  "%20", '!': "%21", '*': "%2A", '\'': "%27", '(': "%28", ')': "%29",
34         ';':  "%3B", ':': "%3A", '@': "%40", '&':  "%26", '=': "%3D", '+': "%2B",
35         '$':  "%24", ',': "%2C", '/': "%2F", '?':  "%3F", '#': "%23", '[': "%5B",
36         ']':  "%5D", '%': "%25",
37     ];
38     return p.translate(translationTable);
39 }
40 unittest {
41     assert(urlEncoded(`abc !#$&'()*+,/:;=?@[]`) == "abc%20%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D");
42 }
43 
44 public class TimeoutException: Exception {
45     this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure {
46         super(msg, file, line);
47     }
48 }
49 
50 public interface Auth {
51     string[string] authHeaders(string domain);
52 }
53 /**
54  * Basic authentication.
55  * Adds $(B Authorization: Basic) header to request.
56  */
57 public class BasicAuthentication: Auth {
58     private {
59         string   _username, _password;
60         string[] _domains;
61     }
62     /// Constructor.
63     /// Params:
64     /// username = username
65     /// password = password
66     /// domains = not used now
67     /// 
68     this(string username, string password, string[] domains = []) {
69         _username = username;
70         _password = password;
71         _domains = domains;
72     }
73     override string[string] authHeaders(string domain) {
74         import std.base64;
75         string[string] auth;
76         auth["Authorization"] = "Basic " ~ to!string(Base64.encode(cast(ubyte[])"%s:%s".format(_username, _password)));
77         return auth;
78     }
79 }
80 
81 
82 ///
83 /// Response - result of request execution.
84 ///
85 /// Response.code - response HTTP code.
86 /// Response.status_line - received HTTP status line.
87 /// Response.responseHeaders - received headers.
88 /// Response.responseBody - container for received body
89 /// Response.history - for redirected responses contain all history
90 /// 
91     public class HTTPResponse : Response {
92     private {
93 //        ushort         __code;
94         string         __status_line;
95         string[string] __responseHeaders;
96 //        Buffer!ubyte   __responseBody;
97         HTTPResponse[]     __history; // redirects history
98         SysTime        __startedAt, __connectedAt, __requestSentAt, __finishedAt;
99     }
100    ~this() {
101         __responseHeaders = null;
102         __history.length = 0;
103     }
104     mixin(getter("code"));
105     mixin(getter("status_line"));
106     mixin(getter("responseHeaders"));
107 //    @property auto responseBody() inout pure @safe nothrow {
108 //        return __responseBody;
109 //    }
110     mixin(getter("history"));
111     private {
112         mixin(setter("code"));
113         mixin(setter("status_line"));
114         mixin(setter("responseHeaders"));
115     }
116     @property auto getStats() const pure @safe {
117         alias statTuple = Tuple!(Duration, "connectTime",
118                                  Duration, "sendTime",
119                                  Duration, "recvTime");
120         statTuple stat;
121         stat.connectTime = __connectedAt - __startedAt;
122         stat.sendTime = __requestSentAt - __connectedAt;
123         stat.recvTime = __finishedAt - __requestSentAt;
124         return stat;
125     }
126 }
127 /**
128  * Struct to send multiple files in POST request.
129  */
130 public struct PostFile {
131     /// Path to the file to send.
132     string fileName;
133     /// Name of the field (if empty - send file base name)
134     string fieldName;
135     /// contentType of the file if not empty
136     string contentType;
137 }
138 ///
139 /// Request.
140 /// Configurable parameters:
141 /// $(B headers) - add any additional headers you'd like to send.
142 /// $(B authenticator) - class to send auth headers.
143 /// $(B keepAlive) - set true for keepAlive requests. default false.
144 /// $(B maxRedirects) - maximum number of redirects. default 10.
145 /// $(B maxHeadersLength) - maximum length of server response headers. default = 32KB.
146 /// $(B maxContentLength) - maximun content length. delault = 5MB.
147 /// $(B bufferSize) - send and receive buffer size. default = 16KB.
148 /// $(B verbosity) - level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0.
149 /// $(B proxy) - set proxy url if needed. default - null.
150 /// 
151 public struct HTTPRequest {
152     private {
153         enum           __preHeaders = [
154             "Accept-Encoding": "gzip, deflate",
155             "User-Agent":      "dlang-requests"
156         ];
157         string         __method = "GET";
158         URI            __uri;
159         string[string] __headers;
160         string[]       __filteredHeaders;
161         Auth           __authenticator;
162         bool           __keepAlive = true;
163         uint           __maxRedirects = 10;
164         size_t         __maxHeadersLength = 32 * 1024; // 32 KB
165         size_t         __maxContentLength = 5 * 1024 * 1024; // 5MB
166         ptrdiff_t      __contentLength;
167         SocketStream   __stream;
168         Duration       __timeout = 30.seconds;
169         HTTPResponse       __response;
170         HTTPResponse[]     __history; // redirects history
171         size_t         __bufferSize = 16*1024; // 16k
172         uint           __verbosity = 0;  // 0 - no output, 1 - headers, 2 - headers+body info
173         DataPipe!ubyte __bodyDecoder;
174         DecodeChunked  __unChunker;
175         string         __proxy;
176     }
177 
178     mixin(getter("keepAlive"));
179     mixin(setter("keepAlive"));
180     mixin(getter("method"));
181     mixin(setter("method"));
182     mixin(getter("timeout"));
183     mixin(setter("timeout"));
184     mixin(setter("authenticator"));
185     mixin(getter("maxContentLength"));
186     mixin(setter("maxContentLength"));
187     mixin(getter("maxRedirects"));
188     mixin(setter("maxRedirects"));
189     mixin(getter("maxHeadersLength"));
190     mixin(setter("maxHeadersLength"));
191     mixin(getter("bufferSize"));
192     mixin(setter("bufferSize"));
193     mixin(getter("verbosity"));
194     mixin(setter("verbosity"));
195     mixin(setter("proxy"));
196 
197     this(string uri) {
198         __uri = URI(uri);
199     }
200    ~this() {
201         if ( __stream && __stream.isConnected) {
202             __stream.close();
203         }
204         __stream = null;
205         __headers = null;
206         __authenticator = null;
207         __history = null;
208     }
209 
210     @property void uri(in URI newURI) {
211         handleURLChange(__uri, newURI);
212         __uri = newURI;
213     }
214     /// Add headers to request
215     /// Params:
216     /// headers = headers to send.
217     void addHeaders(in string[string] headers) {
218         foreach(pair; headers.byKeyValue) {
219             __headers[pair.key] = pair.value;
220         }
221     }
222     /// Remove headers from request
223     /// Params:
224     /// headers = headers to remove.
225     void removeHeaders(in string[] headers) pure {
226         __filteredHeaders ~= headers;
227     }
228     ///
229     /// compose headers to send
230     /// 
231     private @property string[string] headers() {
232         string[string] generatedHeaders = __preHeaders;
233 
234         if ( __authenticator ) {
235             __authenticator.
236                 authHeaders(__uri.host).
237                 byKeyValue.
238                 each!(pair => generatedHeaders[pair.key] = pair.value);
239         }
240 
241         generatedHeaders["Connection"] = __keepAlive?"Keep-Alive":"Close";
242         generatedHeaders["Host"] = __uri.host;
243 
244         if ( __uri.scheme !in standard_ports || __uri.port != standard_ports[__uri.scheme] ) {
245             generatedHeaders["Host"] ~= ":%d".format(__uri.port);
246         }
247 
248         __headers.byKey.each!(h => generatedHeaders[h] = __headers[h]);
249 
250         __filteredHeaders.each!(h => generatedHeaders.remove(h));
251 
252         return generatedHeaders;
253     }
254     ///
255     /// Build request string.
256     /// Handle proxy and query parameters.
257     /// 
258     private @property string requestString(string[string] params = null) {
259         if ( __proxy ) {
260             return "%s %s HTTP/1.1\r\n".format(__method, __uri.uri);
261         }
262         auto query = __uri.query.dup;
263         if ( params ) {
264             query ~= params2query(params);
265             if ( query[0] != '?' ) {
266                 query = "?" ~ query;
267             }
268         }
269         return "%s %s%s HTTP/1.1\r\n".format(__method, __uri.path, query);
270     }
271     ///
272     /// encode parameters and build query part of the url
273     /// 
274     private static string params2query(string[string] params) {
275         auto m = params.keys.
276                         sort().
277                         map!(a=>urlEncoded(a) ~ "=" ~ urlEncoded(params[a])).
278                         join("&");
279         return m;
280     }
281     unittest {
282         assert(HTTPRequest.params2query(["c ":"d", "a":"b"])=="a=b&c%20=d");
283     }
284     ///
285     /// Analyze received headers, take appropriate actions:
286     /// check content length, attach unchunk and uncompress
287     /// 
288     private void analyzeHeaders(in string[string] headers) {
289 
290         __contentLength = -1;
291         __unChunker = null;
292         auto contentLength = "content-length" in headers;
293         if ( contentLength ) {
294             try {
295                 __contentLength = to!ptrdiff_t(*contentLength);
296                 if ( __contentLength > maxContentLength) {
297                     throw new RequestException("ContentLength > maxContentLength (%d>%d)".
298                                 format(__contentLength, __maxContentLength));
299                 }
300             } catch (ConvException e) {
301                 throw new RequestException("Can't convert Content-Length from %s".format(*contentLength));
302             }
303         }
304         auto transferEncoding = "transfer-encoding" in headers;
305         if ( transferEncoding ) {
306             tracef("transferEncoding: %s", *transferEncoding);
307             if ( *transferEncoding == "chunked") {
308                 __unChunker = new DecodeChunked();
309                 __bodyDecoder.insert(__unChunker);
310             }
311         }
312         auto contentEncoding = "content-encoding" in headers;
313         if ( contentEncoding ) switch (*contentEncoding) {
314             default:
315                 throw new RequestException("Unknown content-encoding " ~ *contentEncoding);
316             case "gzip":
317             case "deflate":
318                 __bodyDecoder.insert(new Decompressor!ubyte);
319         }
320     }
321     ///
322     /// Called when we know that all headers already received in buffer
323     /// 1. Split headers on lines
324     /// 2. store status line, store response code
325     /// 3. unfold headers if needed
326     /// 4. store headers
327     /// 
328     private void parseResponseHeaders(ref Buffer!ubyte buffer) {
329         string lastHeader;
330         foreach(line; buffer.data!(string).split("\n").map!(l => l.stripRight)) {
331             if ( ! __response.status_line.length ) {
332                 tracef("statusLine: %s", line);
333                 __response.status_line = line;
334                 if ( __verbosity >= 1 ) {
335                     writefln("< %s", line);
336                 }
337                 auto parsed = line.split(" ");
338                 if ( parsed.length >= 3 ) {
339                     __response.code = parsed[1].to!ushort;
340                 }
341                 continue;
342             }
343             if ( line[0] == ' ' || line[0] == '\t' ) {
344                 // unfolding https://tools.ietf.org/html/rfc822#section-3.1
345                 auto stored = lastHeader in __response.__responseHeaders;
346                 if ( stored ) {
347                     *stored ~= line;
348                 }
349                 continue;
350             }
351             auto parsed = line.findSplit(":");
352             auto header = parsed[0].toLower;
353             auto value = parsed[2].strip;
354             auto stored = __response.responseHeaders.get(header, null);
355             if ( stored ) {
356                 value = stored ~ ", " ~ value;
357             }
358             __response.__responseHeaders[header] = value;
359             if ( __verbosity >= 1 ) {
360                 writefln("< %s: %s", parsed[0], value);
361             }
362 
363             tracef("Header %s = %s", header, value);
364             lastHeader = header;
365         }
366     }
367 
368     ///
369     /// Do we received \r\n\r\n?
370     /// 
371     private bool headersHaveBeenReceived(in ubyte[] data, ref Buffer!ubyte buffer, out string separator) pure const @safe {
372         foreach(s; ["\r\n\r\n", "\n\n"]) {
373             if ( data.canFind(s) || buffer.canFind(s) ) {
374                 separator = s;
375                 return true;
376             }
377         }
378         return false;
379     }
380 
381     private bool followRedirectResponse() {
382         if ( __history.length >= __maxRedirects ) {
383             return false;
384         }
385         auto location = "location" in __response.responseHeaders;
386         if ( !location ) {
387             return false;
388         }
389         __history ~= __response;
390         auto connection = "connection" in __response.__responseHeaders;
391         if ( !connection || *connection == "close" ) {
392             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
393             __stream.close();
394         }
395         URI oldURI = __uri;
396         URI newURI = oldURI;
397         try {
398             newURI = URI(*location);
399         } catch (UriException e) {
400             trace("Can't parse Location:, try relative uri");
401             newURI.path = *location;
402             newURI.uri = newURI.recalc_uri;
403         }
404         handleURLChange(oldURI, newURI);
405             oldURI = __response.URI;
406         __uri = newURI;
407         __response = new HTTPResponse;
408         __response.URI = oldURI;
409         __response.finalURI = newURI;
410         return true;
411     }
412     ///
413     /// If uri changed so that we have to change host or port, then we have to close socket stream
414     /// 
415     private void handleURLChange(in URI from, in URI to) {
416         if ( __stream !is null && __stream.isConnected && 
417             ( from.scheme != to.scheme || from.host != to.host || from.port != to.port) ) {
418             tracef("Have to reopen stream, because of URI change");
419             __stream.close();
420         }
421     }
422     
423     private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) {
424         if (url is null && __uri.uri == "" ) {
425             throw new RequestException("No url configured", file, line);
426         }
427         
428         if ( url !is null ) {
429             URI newURI = URI(url);
430             handleURLChange(__uri, newURI);
431             __uri = newURI;
432         }
433     }
434     ///
435     /// Setup connection. Handle proxy and https case
436     /// 
437     private void setupConnection() {
438         if ( !__stream || !__stream.isConnected ) {
439             tracef("Set up new connection");
440             URI   uri;
441             if ( __proxy ) {
442                 // use proxy uri to connect
443                 uri.uri_parse(__proxy);
444             } else {
445                 // use original uri
446                 uri = __uri;
447             }
448             final switch (uri.scheme) {
449                 case "http":
450                     __stream = new TCPSocketStream().connect(uri.host, uri.port, __timeout);
451                     break;
452                 case "https":
453                     __stream = new SSLSocketStream().connect(uri.host, uri.port, __timeout);
454                     break;
455             }
456         } else {
457             tracef("Use old connection");
458         }
459     }
460     ///
461     /// Receive response after request we sent.
462     /// Find headers, split on headers and body, continue to receive body
463     /// 
464     private void receiveResponse() {
465 
466         __stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout);
467         scope(exit) {
468             __stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, 0.seconds);
469         }
470 
471         __bodyDecoder = new DataPipe!ubyte();
472         auto b = new ubyte[__bufferSize];
473         scope(exit) {
474             __bodyDecoder = null;
475             __unChunker = null;
476             b = null;
477         }
478 
479         auto buffer = Buffer!ubyte();
480         Buffer!ubyte ResponseHeaders, partialBody;
481         size_t receivedBodyLength;
482         ptrdiff_t read;
483         string separator;
484         
485         while(true) {
486 
487             read = __stream.receive(b);
488             tracef("read: %d", read);
489             if ( read < 0 ) {
490                 version(Windows) {
491                     if ( errno == 0 ) {
492                         throw new TimeoutException("Timeout receiving headers");
493                     }
494                 }
495                 version(Posix) {
496                     if ( errno == EINTR ) {
497                         continue;
498                     }
499                     if ( errno == EAGAIN ) {
500                         throw new TimeoutException("Timeout receiving headers");
501                     }
502                     throw new ErrnoException("receiving Headers");
503                 }
504             }
505             if ( read == 0 ) {
506                 break;
507             }
508             
509             auto data = b[0..read];
510             buffer.put(data);
511             if ( buffer.length > maxHeadersLength ) {
512                 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength));
513             }
514             if ( headersHaveBeenReceived(data, buffer, separator) ) {
515                 auto s = buffer.data!(ubyte[]).findSplit(separator);
516                 ResponseHeaders = Buffer!ubyte(s[0]);
517                 partialBody = Buffer!ubyte(s[2]);
518                 receivedBodyLength += partialBody.length;
519                 parseResponseHeaders(ResponseHeaders);
520                 break;
521             }
522         }
523         
524         analyzeHeaders(__response.__responseHeaders);
525         __bodyDecoder.put(partialBody);
526 
527         if ( __verbosity >= 2 ) {
528             writefln("< %d bytes of body received", partialBody.length);
529         }
530 
531         if ( __method == "HEAD" ) {
532             // HEAD response have ContentLength, but have no body
533             return;
534         }
535 
536         while( true ) {
537             if ( __contentLength >= 0 && receivedBodyLength >= __contentLength ) {
538                 trace("Body received.");
539                 break;
540             }
541             if ( __unChunker && __unChunker.done ) {
542                 break;
543             }
544             read = __stream.receive(b);
545             if ( read < 0 ) {
546                 version(Posix) {
547                     if ( errno == EINTR ) {
548                         continue;
549                     }
550                 }
551                 if ( errno == EAGAIN ) {
552                     throw new TimeoutException("Timeout receiving body");
553                 }
554                 throw new ErrnoException("receiving body");
555             }
556             if ( __verbosity >= 2 ) {
557                 writefln("< %d bytes of body received", read);
558             }
559             tracef("read: %d", read);
560             if ( read == 0 ) {
561                 trace("read done");
562                 break;
563             }
564             receivedBodyLength += read;
565             __bodyDecoder.put(b[0..read].dup);
566             __response.__responseBody.put(__bodyDecoder.get());
567             tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", receivedBodyLength, __contentLength, __response.__responseBody.length);
568         }
569         __bodyDecoder.flush();
570         __response.__responseBody.put(__bodyDecoder.get());
571     }
572     ///
573     /// execute POST request.
574     /// Send form-urlencoded data
575     /// 
576     /// Parameters:
577     ///     url = url to request
578     ///     rqData = data to send
579     ///  Returns:
580     ///     Response
581     ///  Examples:
582     ///  ------------------------------------------------------------------
583     ///  rs = rq.exec!"POST"("http://httpbin.org/post", ["a":"b", "c":"d"]);
584     ///  ------------------------------------------------------------------
585     ///
586     HTTPResponse exec(string method)(string url, string[string] rqData) if (method=="POST") {
587         //
588         // application/x-www-form-urlencoded
589         //
590         __method = method;
591 
592         __response = new HTTPResponse;
593         checkURL(url);
594         __response.URI = __uri;
595         __response.finalURI = __uri;
596 
597     connect:
598         __response.__startedAt = Clock.currTime;
599         setupConnection();
600         
601         if ( !__stream.isConnected() ) {
602             return __response;
603         }
604         __response.__connectedAt = Clock.currTime;
605 
606         string encoded = params2query(rqData);
607         auto h = headers;
608         h["Content-Type"] = "application/x-www-form-urlencoded";
609         h["Content-Length"] = to!string(encoded.length);
610 
611         Appender!string req;
612         req.put(requestString());
613         h.byKeyValue.
614             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
615                 each!(h => req.put(h));
616         req.put("\r\n");
617         req.put(encoded);
618         trace(req.data);
619 
620         if ( __verbosity >= 1 ) {
621             req.data.splitLines.each!(a => writeln("> " ~ a));
622         }
623 
624         auto rc = __stream.send(req.data());
625         if ( rc == -1 ) {
626             errorf("Error sending request: ", lastSocketError);
627             return __response;
628         }
629         __response.__requestSentAt = Clock.currTime;
630 
631         receiveResponse();
632 
633         __response.__finishedAt = Clock.currTime;
634 
635         auto connection = "connection" in __response.__responseHeaders;
636         if ( !connection || *connection == "close" ) {
637             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
638             __stream.close();
639         }
640         if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) {
641             if ( __method != "GET" ) {
642                 return this.get();
643             }
644             goto connect;
645         }
646         __response.__history = __history;
647         return __response;
648     }
649     ///
650     /// send file(s) using POST
651     /// Parameters:
652     ///     url = url
653     ///     files = array of PostFile structures
654     /// Returns:
655     ///     Response
656     /// Example:
657     /// ---------------------------------------------------------------
658     ///    PostFile[] files = [
659     ///                   {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 
660     ///                   {fileName:"tests/test.txt"}
661     ///               ];
662     ///    rs = rq.exec!"POST"("http://httpbin.org/post", files);
663     /// ---------------------------------------------------------------
664     /// 
665     HTTPResponse exec(string method="POST")(string url, PostFile[] files) {
666         import std.uuid;
667         import std.file;
668         //
669         // application/json
670         //
671         bool restartedRequest = false;
672         
673         __method = method;
674         
675         __response = new HTTPResponse;
676         checkURL(url);
677         __response.URI = __uri;
678         __response.finalURI = __uri;
679  
680     connect:
681         __response.__startedAt = Clock.currTime;
682         setupConnection();
683         
684         if ( !__stream.isConnected() ) {
685             return __response;
686         }
687         __response.__connectedAt = Clock.currTime;
688 
689         Appender!string req;
690         req.put(requestString());
691         
692         string   boundary = randomUUID().toString;
693         string[] partHeaders;
694         size_t   contentLength;
695 
696         foreach(part; files) {
697             string fieldName = part.fieldName ? part.fieldName : part.fileName;
698             string h = "--" ~ boundary ~ "\r\n";
699             h ~= `Content-Disposition: form-data; name="%s"; filename="%s"`.
700                 format(fieldName, part.fileName) ~ "\r\n";
701             if ( part.contentType ) {
702                 h ~= "Content-Type: " ~ part.contentType ~ "\r\n";
703             }
704             h ~= "\r\n";
705             partHeaders ~= h;
706             contentLength += h.length + getSize(part.fileName) + "\r\n".length;
707         }
708         contentLength += "--".length + boundary.length + "--\r\n".length;
709 
710         auto h = headers;
711         h["Content-Type"] = "multipart/form-data; boundary=" ~ boundary;
712         h["Content-Length"] = to!string(contentLength);
713         h.byKeyValue.
714             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
715             each!(h => req.put(h));
716         req.put("\r\n");
717         
718         trace(req.data);
719         if ( __verbosity >= 1 ) {
720             req.data.splitLines.each!(a => writeln("> " ~ a));
721         }
722 
723         auto rc = __stream.send(req.data());
724         if ( rc == -1 ) {
725             errorf("Error sending request: ", lastSocketError);
726             return __response;
727         }
728         foreach(hdr, f; zip(partHeaders, files)) {
729             tracef("sending part headers <%s>", hdr);
730             __stream.send(hdr);
731             auto file = File(f.fileName, "rb");
732             scope(exit) {
733                 file.close();
734             }
735             foreach(chunk; file.byChunk(16*1024)) {
736                 __stream.send(chunk);
737             }
738             __stream.send("\r\n");
739         }
740         __stream.send("--" ~ boundary ~ "--\r\n");
741         __response.__requestSentAt = Clock.currTime;
742 
743         receiveResponse();
744 
745         if ( __response.__responseHeaders.length == 0 
746             && __keepAlive
747             && !restartedRequest
748             && __method == "GET"
749             ) {
750             tracef("Server closed keepalive connection");
751             __stream.close();
752             restartedRequest = true;
753             goto connect;
754         }
755 
756         __response.__finishedAt = Clock.currTime;
757         ///
758         auto connection = "connection" in __response.__responseHeaders;
759         if ( !connection || *connection == "close" ) {
760             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
761             __stream.close();
762         }
763         if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) {
764             if ( __method != "GET" ) {
765                 return this.get();
766             }
767             goto connect;
768         }
769         __response.__history = __history;
770         ///
771         return __response;
772     }
773     ///
774     /// POST data from some string(with Content-Length), or from range of strings (use Transfer-Encoding: chunked)
775     /// 
776     /// Parameters:
777     ///    url = url
778     ///    content = string or input range
779     ///    contentType = content type
780     ///  Returns:
781     ///     Response
782     ///  Examples:
783     ///  ---------------------------------------------------------------------------------------------------------
784     ///      rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream");
785     ///      
786     ///      auto s = lineSplitter("one,\ntwo,\nthree.");
787     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream");
788     ///      
789     ///      auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
790     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream");
791     ///
792     ///      auto f = File("tests/test.txt", "rb");
793     ///      rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream");
794     ///  --------------------------------------------------------------------------------------------------------
795     HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="text/html")
796         if ( (rank!R == 1) //isSomeString!R
797             || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 
798             || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte)))
799             )
800     {
801         //
802         // application/json
803         //
804         bool restartedRequest = false;
805         
806         __method = method;
807         
808         __response = new HTTPResponse;
809         checkURL(url);
810         __response.URI = __uri;
811         __response.finalURI = __uri;
812 
813     connect:
814         __response.__startedAt = Clock.currTime;
815         setupConnection();
816         
817         if ( !__stream.isConnected() ) {
818             return __response;
819         }
820         __response.__connectedAt = Clock.currTime;
821 
822         Appender!string req;
823         req.put(requestString());
824 
825         auto h = headers;
826         h["Content-Type"] = contentType;
827         static if ( rank!R == 1 ) {
828             h["Content-Length"] = to!string(content.length);
829         } else {
830             h["Transfer-Encoding"] = "chunked";
831         }
832         h.byKeyValue.
833             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
834             each!(h => req.put(h));
835         req.put("\r\n");
836 
837         trace(req.data);
838         if ( __verbosity >= 1 ) {
839             req.data.splitLines.each!(a => writeln("> " ~ a));
840         }
841 
842         auto rc = __stream.send(req.data());
843         if ( rc == -1 ) {
844             errorf("Error sending request: ", lastSocketError);
845             return __response;
846         }
847 
848         static if ( rank!R == 1) {
849             __stream.send(content);
850         } else {
851             while ( !content.empty ) {
852                 auto chunk = content.front;
853                 auto chunkHeader = "%x\r\n".format(chunk.length);
854                 tracef("sending %s%s", chunkHeader, chunk);
855                 __stream.send(chunkHeader);
856                 __stream.send(chunk);
857                 __stream.send("\r\n");
858                 content.popFront;
859             }
860             tracef("sent");
861             __stream.send("0\r\n\r\n");
862         }
863         __response.__requestSentAt = Clock.currTime;
864 
865         receiveResponse();
866 
867         if ( __response.__responseHeaders.length == 0 
868             && __keepAlive
869             && !restartedRequest
870             && __method == "GET"
871             ) {
872             tracef("Server closed keepalive connection");
873             __stream.close();
874             restartedRequest = true;
875             goto connect;
876         }
877 
878         __response.__finishedAt = Clock.currTime;
879 
880         ///
881         auto connection = "connection" in __response.__responseHeaders;
882         if ( !connection || *connection == "close" ) {
883             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
884             __stream.close();
885         }
886         if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) {
887             if ( __method != "GET" ) {
888                 return this.get();
889             }
890             goto connect;
891         }
892         ///
893         __response.__history = __history;
894         return __response;
895     }
896     ///
897     /// Send request without data
898     /// Request parameters will be encoded into request string
899     /// Parameters:
900     ///     url = url
901     ///     params = request parameters
902     ///  Returns:
903     ///     Response
904     ///  Examples:
905     ///  ---------------------------------------------------------------------------------
906     ///     rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]);
907     ///  ---------------------------------------------------------------------------------
908     ///     
909     HTTPResponse exec(string method="GET")(string url = null, string[string] params = null) if (method != "POST")
910     {
911 
912         __method = method;
913         __response = new HTTPResponse;
914         __history.length = 0;
915         bool restartedRequest = false; // True if this is restarted keepAlive request
916 
917         checkURL(url);
918         __response.URI = __uri;
919         __response.finalURI = __uri;
920     connect:
921         __response.__startedAt = Clock.currTime;
922         setupConnection();
923 
924         if ( !__stream.isConnected() ) {
925             return __response;
926         }
927         __response.__connectedAt = Clock.currTime;
928 
929         Appender!string req;
930         req.put(requestString(params));
931         headers.byKeyValue.
932             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
933             each!(h => req.put(h));
934         req.put("\r\n");
935         trace(req.data);
936 
937         if ( __verbosity >= 1 ) {
938             req.data.splitLines.each!(a => writeln("> " ~ a));
939         }
940         auto rc = __stream.send(req.data());
941         if ( rc == -1 ) {
942             errorf("Error sending request: ", lastSocketError);
943             return __response;
944         }
945         __response.__requestSentAt = Clock.currTime;
946 
947         receiveResponse();
948 
949         if ( __response.__responseHeaders.length == 0 
950             && __keepAlive
951             && !restartedRequest
952             && __method == "GET"
953         ) {
954             tracef("Server closed keepalive connection");
955             __stream.close();
956             restartedRequest = true;
957             goto connect;
958         }
959         __response.__finishedAt = Clock.currTime;
960 
961         ///
962         auto connection = "connection" in __response.__responseHeaders;
963         if ( !connection || *connection == "close" ) {
964             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
965             __stream.close();
966         }
967         if ( __verbosity >= 1 ) {
968             writeln(">> Connect time: ", __response.__connectedAt - __response.__startedAt);
969             writeln(">> Request send time: ", __response.__requestSentAt - __response.__connectedAt);
970             writeln(">> Response recv time: ", __response.__finishedAt - __response.__requestSentAt);
971         }
972         if ( canFind(redirectCodes, __response.__code) && followRedirectResponse() ) {
973             if ( __method != "GET" ) {
974                 return this.get();
975             }
976             goto connect;
977         }
978         ///
979         __response.__history = __history;
980         return __response;
981     }
982     ///
983     /// GET request. Simple wrapper over exec!"GET"
984     /// Params:
985     /// args = request parameters. see exec docs.
986     ///
987     HTTPResponse get(A...)(A args) {
988         return exec!"GET"(args);
989     }
990     ///
991     /// POST request. Simple wrapper over exec!"POST"
992     /// Params:
993     /// args = request parameters. see exec docs.
994     ///
995     HTTPResponse post(A...)(string uri, A args) {
996         return exec!"POST"(uri, args);
997     }
998 }
999 
1000 ///
1001 public unittest {
1002     import std.json;
1003     globalLogLevel(LogLevel.info);
1004     tracef("http tests - start");
1005 
1006     auto rq = HTTPRequest();
1007     auto rs = rq.get("https://httpbin.org/");
1008     assert(rs.code==200);
1009     assert(rs.responseBody.length > 0);
1010     rs = HTTPRequest().get("http://httpbin.org/get", ["c":" d", "a":"b"]);
1011     assert(rs.code == 200);
1012     auto json = parseJSON(rs.responseBody.data).object["args"].object;
1013     assert(json["c"].str == " d");
1014     assert(json["a"].str == "b");
1015 
1016     globalLogLevel(LogLevel.info);
1017     rq = HTTPRequest();
1018     rq.keepAlive = true;
1019     // handmade json
1020     info("Check POST json");
1021     rs = rq.post("http://httpbin.org/post?b=x", `{"a":"☺ ", "c":[1,2,3]}`, "application/json");
1022     assert(rs.code==200);
1023     json = parseJSON(rs.responseBody.data).object["args"].object;
1024     assert(json["b"].str == "x");
1025     json = parseJSON(rs.responseBody.data).object["json"].object;
1026     assert(json["a"].str == "☺ ");
1027     assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]);
1028     {
1029         import std.file;
1030         import std.path;
1031         auto tmpd = tempDir();
1032         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
1033         auto f = File(tmpfname, "wb");
1034         f.rawWrite("abcdefgh\n12345678\n");
1035         f.close();
1036         // files
1037         globalLogLevel(LogLevel.info);
1038         info("Check POST files");
1039         PostFile[] files = [
1040                         {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 
1041                         {fileName: tmpfname}
1042                     ];
1043         rs = rq.post("http://httpbin.org/post", files);
1044         assert(rs.code==200);
1045         info("Check POST chunked from file.byChunk");
1046         f = File(tmpfname, "rb");
1047         rs = rq.post("http://httpbin.org/post", f.byChunk(3), "application/octet-stream");
1048         assert(rs.code==200);
1049         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1050         assert(data=="abcdefgh\n12345678\n");
1051         f.close();
1052     }
1053     {
1054         // string
1055         info("Check POST utf8 string");
1056         rs = rq.post("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream");
1057         assert(rs.code==200);
1058         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1059         assert(data=="привiт, свiт!");
1060     }
1061     // ranges
1062     {
1063         info("Check POST chunked from lineSplitter");
1064         auto s = lineSplitter("one,\ntwo,\nthree.");
1065         rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream");
1066         assert(rs.code==200);
1067         auto data = parseJSON(rs.responseBody.toString).object["data"].str;
1068         assert(data=="one,two,three.");
1069     }
1070     {
1071         info("Check POST chunked from array");
1072         auto s = ["one,", "two,", "three."];
1073         rs = rq.post("http://httpbin.org/post", s, "application/octet-stream");
1074         assert(rs.code==200);
1075         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1076         assert(data=="one,two,three.");
1077     }
1078     {
1079         info("Check POST chunked using std.range.chunks()");
1080         auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1081         rs = rq.post("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream");
1082         assert(rs.code==200);
1083         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1084         assert(data==s);
1085     }
1086     // associative array
1087     rs = rq.post("http://httpbin.org/post", ["a":"b ", "c":"d"]);
1088     assert(rs.code==200);
1089     auto form = parseJSON(rs.responseBody.data).object["form"].object;
1090     assert(form["a"].str == "b ");
1091     assert(form["c"].str == "d");
1092     info("Check HEAD");
1093     rs = rq.exec!"HEAD"("http://httpbin.org/");
1094     assert(rs.code==200);
1095     info("Check DELETE");
1096     rs = rq.exec!"DELETE"("http://httpbin.org/delete");
1097     assert(rs.code==200);
1098     info("Check PUT");
1099     rs = rq.exec!"PUT"("http://httpbin.org/put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
1100     assert(rs.code==200);
1101     info("Check PATCH");
1102     rs = rq.exec!"PATCH"("http://httpbin.org/patch", "привiт, свiт!", "application/octet-stream");
1103     assert(rs.code==200);
1104 
1105     info("Check compressed content");
1106     globalLogLevel(LogLevel.info);
1107     rq = HTTPRequest();
1108     rq.keepAlive = true;
1109     rs = rq.get("http://httpbin.org/gzip");
1110     assert(rs.code==200);
1111     info("gzip - ok");
1112     rs = rq.get("http://httpbin.org/deflate");
1113     assert(rs.code==200);
1114     info("deflate - ok");
1115 
1116     info("Check redirects");
1117     globalLogLevel(LogLevel.info);
1118     rq = HTTPRequest();
1119     rq.keepAlive = true;
1120     rs = rq.get("http://httpbin.org/relative-redirect/2");
1121     assert(rs.history.length == 2);
1122     assert(rs.code==200);
1123 //    rq = Request();
1124 //    rq.keepAlive = true;
1125 //    rq.proxy = "http://localhost:8888/";
1126     rs = rq.get("http://httpbin.org/absolute-redirect/2");
1127     assert(rs.history.length == 2);
1128     assert(rs.code==200);
1129 //    rq = Request();
1130     rq.maxRedirects = 2;
1131     rq.keepAlive = false;
1132     rs = rq.get("https://httpbin.org/absolute-redirect/3");
1133     assert(rs.history.length == 2);
1134     assert(rs.code==302);
1135 
1136     info("Check utf8 content");
1137     globalLogLevel(LogLevel.info);
1138     rq = HTTPRequest();
1139     rs = rq.get("http://httpbin.org/encoding/utf8");
1140     assert(rs.code==200);
1141 
1142     info("Check chunked content");
1143     globalLogLevel(LogLevel.info);
1144     rq = HTTPRequest();
1145     rq.keepAlive = true;
1146     rq.bufferSize = 16*1024;
1147     rs = rq.get("http://httpbin.org/range/1024");
1148     assert(rs.code==200);
1149     assert(rs.responseBody.length==1024);
1150 
1151     info("Check basic auth");
1152     globalLogLevel(LogLevel.info);
1153     rq = HTTPRequest();
1154     rq.authenticator = new BasicAuthentication("user", "passwd");
1155     rs = rq.get("http://httpbin.org/basic-auth/user/passwd");
1156     assert(rs.code==200);
1157  
1158     globalLogLevel(LogLevel.info);
1159     info("Check exception handling, error messages are OK");
1160     rq = HTTPRequest();
1161     rq.timeout = 1.seconds;
1162     assertThrown!TimeoutException(rq.get("http://httpbin.org/delay/3"));
1163     assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/"));
1164     assertThrown!ConnectError(rq.get("http://1.1.1.1/"));
1165     //assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/"));
1166 
1167     globalLogLevel(LogLevel.info);
1168     info("Check limits");
1169     rq = HTTPRequest();
1170     rq.maxContentLength = 1;
1171     assertThrown!RequestException(rq.get("http://httpbin.org/"));
1172     rq = HTTPRequest();
1173     rq.maxHeadersLength = 1;
1174     assertThrown!RequestException(rq.get("http://httpbin.org/"));
1175     tracef("http tests - ok");
1176 }