1 module requests;
2 
3 public import requests.http;
4 public import requests.ftp;
5 public import requests.streams;
6 public import requests.base;
7 public import requests.uri;
8 
9 import std.datetime;
10 import std.conv;
11 import std.experimental.logger;
12 import requests.utils;
13 
14 /**
15    This is simplest interface to both http and ftp protocols.
16    Request has methods get, post and exec which routed to proper concrete handler (http or ftp, etc).
17    To enable some protocol-specific featutes you have to use protocol interface directly (see docs for HTTPRequest or FTPRequest)
18 */
19 public struct Request {
20     private {
21         URI         _uri;
22         HTTPRequest _http;  // route all http/https requests here
23         FTPRequest  _ftp;   // route all ftp requests here
24     }
25     /// Set timeout on IO operation.
26     /// $(B v) - timeout value
27     /// 
28     public @property void timeout(Duration v) pure @nogc nothrow {
29         _http.timeout = v;
30         _ftp.timeout = v;
31     }
32     /// Set http keepAlive value
33     /// $(B v) - use keepalive requests - $(B true), or not - $(B false)
34     @property void keepAlive(bool v) pure @nogc nothrow {
35         _http.keepAlive = v;
36     }
37     /// Set limit on HTTP redirects
38     /// $(B v) - limit on redirect depth
39     @property void maxRedirects(uint v) pure @nogc nothrow {
40         _http.maxRedirects = v;
41     }
42     /// Set maximum content lenth both for http and ftp requests
43     /// $(B v) - maximum content length in bytes. When limit reached - throw RequestException
44     @property void maxContentLength(size_t v) pure @nogc nothrow {
45         _http.maxContentLength = v;
46         _ftp.maxContentLength = v;
47     }
48     /// Set maximum length for HTTP headers
49     /// $(B v) - maximum length of the HTTP response. When limit reached - throw RequestException
50     @property void maxHeadersLength(size_t v) pure @nogc nothrow {
51         _http.maxHeadersLength = v;
52     }
53     /// Set IO buffer size for http and ftp requests
54     /// $(B v) - buffer size in bytes.
55     @property void bufferSize(size_t v) {
56         _http.bufferSize = v;
57         _ftp.bufferSize = v;
58     }
59     /// Set verbosity for HTTP or FTP requests.
60     /// $(B v) - verbosity level (0 - no output, 1 - headers to stdout, 2 - headers and body progress to stdout). default = 0.
61     @property void verbosity(uint v) {
62         _http.verbosity = v;
63         _ftp.verbosity = v;
64     }
65     /// Set authenticator for http requests.
66     /// $(B v) - Auth instance.
67     @property void authenticator(Auth v) {
68         _http.authenticator = v;
69     }
70     /// Set Cookie for http requests.
71     /// $(B v) - array of cookie.
72     @property void cookie(Cookie[] v) pure @nogc nothrow {
73         _http.cookie = v;
74     }
75     /// Get Cookie for http requests.
76     /// $(B v) - array of cookie.
77     @property Cookie[] cookie()  pure @nogc nothrow {
78         return _http.cookie;
79     }
80     ///
81     /// set "streaming" property
82     /// Params:
83     /// v = value to set (true - use streaming)
84     /// 
85     @property void useStreaming(bool v) pure @nogc nothrow {
86         _http.useStreaming = v;
87         _ftp.useStreaming = v;
88     }
89     ///
90     /// get length og actually received content.
91     /// this value increase over time, while we receive data
92     /// 
93     @property long contentReceived() pure @nogc nothrow {
94         final switch ( _uri.scheme ) {
95             case "http", "https":
96                 return _http.contentReceived;
97             case "ftp":
98                 return _ftp.contentReceived;
99         }
100     }
101     /// get contentLength of the responce
102     @property long contentLength() pure @nogc nothrow {
103         final switch ( _uri.scheme ) {
104             case "http", "https":
105                 return _http.contentLength;
106             case "ftp":
107                 return _ftp.contentLength;
108         }
109     }
110     @property void sslSetVerifyPeer(bool v) {
111         _http.sslSetVerifyPeer(v);
112     }
113     @property void sslSetKeyFile(string path, SSLOptions.filetype type = SSLOptions.filetype.pem) pure @safe nothrow @nogc {
114         _http.sslSetKeyFile(path, type);
115     }
116     @property void sslSetCertFile(string path, SSLOptions.filetype type = SSLOptions.filetype.pem) pure @safe nothrow @nogc {
117         _http.sslSetCertFile(path, type);
118     }
119     @property void sslSetCaCert(string path) pure @safe nothrow @nogc {
120         _http.sslSetCaCert(path);
121     }
122     @property auto sslOptions() {
123         return _http.sslOptions();
124     }
125     /// Add headers to request
126     /// Params:
127     /// headers = headers to send.
128     void addHeaders(in string[string] headers) {
129         _http.addHeaders(headers);
130     }
131     /// Execute GET for http and retrieve file for FTP.
132     /// You have to provide at least $(B uri). All other arguments should conform to HTTPRequest.get or FTPRequest.get depending on the URI scheme.
133     /// When arguments do not conform scheme (for example you try to call get("ftp://somehost.net/pub/README", {"a":"b"}) which doesn't make sense)
134     /// you will receive Exception("Operation not supported for ftp")
135     ///
136     Response get(A...)(string uri, A args) {
137         if ( uri ) {
138             _uri = URI(uri);
139         }
140         final switch ( _uri.scheme ) {
141             case "http", "https":
142                 _http.uri = _uri;
143                 static if (__traits(compiles, _http.get(null, args))) {
144                     return _http.get(null, args);
145                 } else {
146                     throw new Exception("Operation not supported for http");
147                 }
148             case "ftp":
149                 static if (args.length == 0) {
150                     return _ftp.get(uri);
151                 } else {
152                     throw new Exception("Operation not supported for ftp");
153                 }
154         }
155     }
156     /// Execute POST for http and STOR file for FTP.
157     /// You have to provide  $(B uri) and data. Data should conform to HTTPRequest.post or FTPRequest.post depending on the URI scheme.
158     /// When arguments do not conform scheme you will receive Exception("Operation not supported for ftp")
159     ///
160     Response post(A...)(string uri, A args) {
161         if ( uri ) {
162             _uri = URI(uri);
163         }
164         final switch ( _uri.scheme ) {
165             case "http", "https":
166                 _http.uri = _uri;
167                 static if (__traits(compiles, _http.post(null, args))) {
168                     return _http.post(null, args);
169                 } else {
170                     throw new Exception("Operation not supported for http");
171                 }
172             case "ftp":
173                 static if (__traits(compiles, _ftp.post(uri, args))) {
174                     return _ftp.post(uri, args);
175                 } else {
176                     throw new Exception("Operation not supported for ftp");
177                 }
178         }
179     }
180     Response exec(string method="GET", A...)(A args) {
181         return _http.exec!(method)(args);
182     }
183 }
184 ///
185 package unittest {
186     import std.algorithm;
187     import std.range;
188     import std.array;
189     import std.json;
190     import std.stdio;
191     import std.string;
192     import std.exception;
193 
194     string httpbinUrl = httpTestServer();
195 
196     version(vibeD) {
197     }
198     else {
199         import httpbin;
200         auto server = httpbinApp();
201         server.start();
202         scope(exit) {
203             server.stop();
204         }
205     }
206 
207     globalLogLevel(LogLevel.info);
208 
209     infof("testing Request");
210     Request rq;
211     Response rs;
212     //
213     rs = rq.get(httpbinUrl);
214     assert(rs.code==200);
215     assert(rs.responseBody.length > 0);
216     rs = rq.get(httpbinUrl ~ "get", ["c":" d", "a":"b"]);
217     assert(rs.code == 200);
218     auto json = parseJSON(rs.responseBody.data).object["args"].object;
219     assert(json["c"].str == " d");
220     assert(json["a"].str == "b");
221     
222     rq = Request();
223     rq.keepAlive = true;
224     // handmade json
225     info("Check POST json");
226     rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"b ", "c":[1,2,3]}`, "application/json");
227     assert(rs.code==200);
228     json = parseJSON(rs.responseBody.data).object["args"].object;
229     assert(json["b"].str == "x");
230     json = parseJSON(rs.responseBody.data).object["json"].object;
231     assert(json["a"].str == "b ");
232     assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]);
233     {
234         import std.file;
235         import std.path;
236         auto tmpd = tempDir();
237         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
238         auto f = File(tmpfname, "wb");
239         f.rawWrite("abcdefgh\n12345678\n");
240         f.close();
241         // files
242         info("Check POST files");
243         PostFile[] files = [
244             {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 
245             {fileName: tmpfname}
246         ];
247         rs = rq.post(httpbinUrl ~ "post", files);
248         assert(rs.code==200);
249         info("Check POST chunked from file.byChunk");
250         f = File(tmpfname, "rb");
251         rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream");
252         assert(rs.code==200);
253         auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]);
254         assert(data=="abcdefgh\n12345678\n");
255         f.close();
256     }
257     // ranges
258     {
259         info("Check POST chunked from lineSplitter");
260         auto s = lineSplitter("one,\ntwo,\nthree.");
261         rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream");
262         assert(rs.code==200);
263         auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]);
264         assert(data=="one,two,three.");
265     }
266     {
267         info("Check POST chunked from array");
268         auto s = ["one,", "two,", "three."];
269         rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream");
270         assert(rs.code==200);
271         auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]);
272         assert(data=="one,two,three.");
273     }
274     {
275         info("Check POST chunked using std.range.chunks()");
276         auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
277         rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
278         assert(rs.code==200);
279         auto data = fromJsonArrayToStr(parseJSON(rs.responseBody).object["data"]);
280         assert(data==s);
281     }
282     // associative array
283     rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]);
284     assert(rs.code==200);
285     auto form = parseJSON(rs.responseBody.data).object["form"].object;
286     assert(form["a"].str == "b ");
287     assert(form["c"].str == "d");
288     info("Check HEAD");
289     rs = rq.exec!"HEAD"(httpbinUrl);
290     assert(rs.code==200);
291     info("Check DELETE");
292     rs = rq.exec!"DELETE"(httpbinUrl ~ "delete");
293     assert(rs.code==200);
294     info("Check PUT");
295     rs = rq.exec!"PUT"(httpbinUrl ~ "put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
296     assert(rs.code==200);
297     info("Check PATCH");
298     rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream");
299     assert(rs.code==200);
300     
301     info("Check compressed content");
302     rq = Request();
303     rq.keepAlive = true;
304     rq.addHeaders(["X-Header": "test"]);
305     rs = rq.get(httpbinUrl ~ "gzip");
306     assert(rs.code==200);
307     info("gzip - ok");
308     rs = rq.get(httpbinUrl ~ "deflate");
309     assert(rs.code==200);
310     info("deflate - ok");
311     
312     info("Check redirects");
313     rq = Request();
314     rq.keepAlive = true;
315     rs = rq.get(httpbinUrl ~ "relative-redirect/2");
316     assert((cast(HTTPResponse)rs).history.length == 2);
317     assert((cast(HTTPResponse)rs).code==200);
318 
319     info("Check cookie");
320     rq = Request();
321     rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef");
322     assert(rs.code == 200);
323     json = parseJSON(rs.responseBody.data).object["cookies"].object;
324     assert(json["A"].str == "abcd");
325     assert(json["b"].str == "cdef");
326     auto cookie = rq.cookie();
327     foreach(c; rq.cookie) {
328         final switch(c.attr) {
329             case "A":
330                 assert(c.value == "abcd");
331                 break;
332             case "b":
333                 assert(c.value == "cdef");
334                 break;
335         }
336     }
337     rs = rq.get(httpbinUrl ~ "absolute-redirect/2");
338     assert((cast(HTTPResponse)rs).history.length == 2);
339     assert((cast(HTTPResponse)rs).code==200);
340     //    rq = Request();
341     rq.maxRedirects = 2;
342     rq.keepAlive = false;
343     assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3"));
344 
345     info("Check chunked content");
346     rq = Request();
347     rq.keepAlive = true;
348     rq.bufferSize = 16*1024;
349     rs = rq.get(httpbinUrl ~ "range/1024");
350     assert(rs.code==200);
351     assert(rs.responseBody.length==1024);
352     
353     info("Check basic auth");
354     rq = Request();
355     rq.authenticator = new BasicAuthentication("user", "passwd");
356     rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd");
357     assert(rs.code==200);
358     
359     info("Check limits");
360     rq = Request();
361     rq.maxContentLength = 1;
362     assertThrown!RequestException(rq.get(httpbinUrl));
363     rq = Request();
364     rq.maxHeadersLength = 1;
365     assertThrown!RequestException(rq.get(httpbinUrl));
366 
367     info("Test getContent");
368     auto r = getContent(httpbinUrl ~ "stream/20");
369     assert(r.splitter('\n').filter!("a.length>0").count == 20);
370     r = getContent(httpbinUrl ~ "get", ["a":"b", "c":"d"]);
371     string name = "user", sex = "male";
372     int    age = 42;
373     r = getContent(httpbinUrl ~ "get", "name", name, "age", age, "sex", sex);
374 
375     info("Test receiveAsRange with GET");
376     rq = Request();
377     rq.useStreaming = true;
378     rq.bufferSize = 16;
379     rs = rq.get(httpbinUrl ~ "stream/20");
380     auto stream = rs.receiveAsRange();
381     ubyte[] streamedContent;
382     while( !stream.empty() ) {
383         streamedContent ~= stream.front;
384         stream.popFront();
385     }
386     rq = Request();
387     rs = rq.get(httpbinUrl ~ "stream/20");
388     assert(streamedContent == rs.responseBody.data);
389     info("Test postContent");
390     r = postContent(httpbinUrl ~ "post", `{"a":"b", "c":1}`, "application/json");
391     assert(parseJSON(r).object["json"].object["c"].integer == 1);
392 
393     /// Posting to forms (for small data)
394     ///
395     /// posting query parameters using "application/x-www-form-urlencoded"
396     info("Test postContent using query params");
397     postContent(httpbinUrl ~ "post", queryParams("first", "a", "second", 2));
398     
399     /// posting using multipart/form-data (large data and files). See docs fot HTTPRequest
400     info("Test postContent form");
401     MultipartForm mpform;
402     mpform.add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
403     postContent(httpbinUrl ~ "post", mpform);
404     
405     /// you can do this using Request struct to access response details
406     info("Test postContent form via Request()");
407     rq = Request();
408     mpform = MultipartForm().add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
409     rs = rq.post(httpbinUrl ~ "post", mpform);
410     assert(rs.code == 200);
411     
412     info("Test receiveAsRange with POST");
413     streamedContent.length = 0;
414     rq = Request();
415     rq.useStreaming = true;
416     rq.bufferSize = 16;
417     string s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
418     rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
419     stream = rs.receiveAsRange();
420     while( !stream.empty() ) {
421         streamedContent ~= stream.front;
422         stream.popFront();
423     }
424     rq = Request();
425     rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
426     assert(streamedContent == rs.responseBody.data);
427     info("Test get in parallel");
428     {
429         import std.stdio;
430         import std.parallelism;
431         import std.algorithm;
432         import std.string;
433         import core.atomic;
434         
435         immutable auto urls = [
436             "stream/10",
437             "stream/20",
438             "stream/30",
439             "stream/40",
440             "stream/50",
441             "stream/60",
442             "stream/70",
443         ].map!(a => httpbinUrl ~ a).array.idup;
444         
445         defaultPoolThreads(4);
446         
447         shared short lines;
448         
449         foreach(url; parallel(urls)) {
450             atomicOp!"+="(lines, getContent(url).splitter("\n").count);
451         }
452         assert(lines == 287);
453         
454     }
455 }
456 
457 auto queryParams(A...)(A args) pure @safe nothrow {
458     QueryParam[] res;
459     static if ( args.length >= 2 ) {
460         res = QueryParam(args[0].to!string, args[1].to!string) ~ queryParams(args[2..$]);
461     }
462     return res;
463 }
464 /**
465  * Call GET, and return response content.
466  * This is the simplest case, when all you need is the response body and have no parameters.
467  * Returns:
468  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
469  */
470 public auto ref getContent(A...)(string url) {
471     auto rq = Request();
472     auto rs = rq.get(url);
473     return rs.responseBody;
474 }
475 /**
476  * Call GET, and return response content.
477  * args = string[string] fo query parameters.
478  * Returns:
479  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
480  */
481 public auto ref getContent(A...)(string url, string[string] args) {
482     auto rq = Request();
483     auto rs = rq.get(url, args);
484     return rs.responseBody;
485 }
486 /**
487  * Call GET, and return response content.
488  * args = QueryParam[] of parameters.
489  * Returns:
490  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
491  */
492 public auto ref getContent(A...)(string url, QueryParam[] args) {
493     auto rq = Request();
494     auto rs = rq.get(url, args);
495     return rs.responseBody;
496 }
497 /**
498  * Call GET, and return response content.
499  * args = variadic args to supply parameter names and values.
500  * Returns:
501  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
502  */
503 public auto ref getContent(A...)(string url, A args) if (args.length > 1 && args.length % 2 == 0 ) {
504     return Request().
505             get(url, queryParams(args)).
506             responseBody;
507 }
508 
509 ///
510 /// Call post and return response content.
511 ///
512 public auto postContent(A...)(string url, A args) {
513     auto rq = Request();
514     auto rs = rq.post(url, args);
515     return rs.responseBody;
516 }
517 
518 ///
519 package unittest {
520     import std.json;
521     import std.string;
522     import std.stdio;
523     import std.range;
524 
525     globalLogLevel(LogLevel.info);
526 
527     /// ftp upload from range
528     info("Test postContent ftp");
529     auto r = postContent("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
530     assert(r.length == 0);
531 
532     info("Test getContent(ftp)");
533     r = getContent("ftp://speedtest.tele2.net/1KB.zip");
534     assert(r.length == 1024);
535     
536     info("Test receiveAsRange with GET(ftp)");
537     ubyte[] streamedContent;
538     auto rq = Request();
539     rq.useStreaming = true;
540     streamedContent.length = 0;
541     auto rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
542     auto stream = rs.receiveAsRange;
543     while( !stream.empty() ) {
544         streamedContent ~= stream.front;
545         stream.popFront();
546     }
547     assert(streamedContent.length == 1024);
548     //
549     info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
550     rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
551     assert(rs.code == 226);
552     info("ftp get  ", "ftp://speedtest.tele2.net/nonexistent", ", in same session.");
553     rs = rq.get("ftp://speedtest.tele2.net/nonexistent");
554     assert(rs.code != 226);
555     rq.useStreaming = false;
556     info("ftp get  ", "ftp://speedtest.tele2.net/1KB.zip", ", in same session.");
557     rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
558     assert(rs.code == 226);
559     assert(rs.responseBody.length == 1024);
560     info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
561     rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "another test, ignore please\n".representation);
562     assert(rs.code == 226);
563     info("ftp get  ", "ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
564     rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
565     assert(rs.code == 226);
566     info("testing ftp - done.");
567 }
568