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 public import requests.request;
9 public import requests.pool;
10 public import requests.utils;
11 
12 import std.datetime;
13 import std.conv;
14 import std.experimental.logger;
15 import requests.utils;
16 
17 ///
18 package unittest {
19     import std.algorithm;
20     import std.range;
21     import std.array;
22     import std.json;
23     import std.stdio;
24     import std.string;
25     import std.exception;
26 
27     string httpbinUrl = httpTestServer();
28     globalLogLevel(LogLevel.info);
29 
30     version(vibeD) {
31     }
32     else {
33         import httpbin;
34         auto server = httpbinApp();
35         server.start();
36         scope(exit) {
37             server.stop();
38         }
39     }
40 
41 
42     infof("testing Request");
43     Request rq;
44     Response rs;
45     // moved from http.d
46 
47     URI uri = URI(httpbinUrl);
48     rs = rq.get(httpbinUrl);
49     assert(rs.code==200);
50     assert(rs.responseBody.length > 0);
51     assert(rq.format("%m|%h|%p|%P|%q|%U") ==
52     "GET|%s|%d|%s||%s"
53     .format(uri.host, uri.port, uri.path, httpbinUrl));
54 
55     //
56     rs = rq.get(httpbinUrl);
57     assert(rs.code==200);
58     assert(rs.responseBody.length > 0);
59     rs = rq.get(httpbinUrl ~ "get", ["c":" d", "a":"b"]);
60     assert(rs.code == 200);
61     auto json = parseJSON(cast(string)rs.responseBody.data).object["args"].object;
62     assert(json["c"].str == " d");
63     assert(json["a"].str == "b");
64 
65     
66     rq = Request();
67     rq.keepAlive = false; // disable keepalive on idempotents requests
68     {
69         info("Check handling incomplete status line");
70         rs = rq.get(httpbinUrl ~ "incomplete");
71         if (httpbinUrl != "http://httpbin.org/") {
72             // 600 when test direct server respond, or 502 if test through squid
73             assert(rs.code==600 || rs.code == 502);
74         }
75     }
76     // handmade json
77     info("Check POST json");
78     rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"b ", "c":[1,2,3]}`, "application/json");
79     assert(rs.code==200);
80     json = parseJSON(cast(string)rs.responseBody.data).object["args"].object;
81     assert(json["b"].str == "x");
82     json = parseJSON(cast(string)rs.responseBody.data).object["json"].object;
83     assert(json["a"].str == "b ");
84     assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]);
85     {
86         import std.file;
87         import std.path;
88         auto tmpd = tempDir();
89         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
90         auto f = File(tmpfname, "wb");
91         f.rawWrite("abcdefgh\n12345678\n");
92         f.close();
93         // files
94         info("Check POST files");
95         PostFile[] files = [
96             {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 
97             {fileName: tmpfname}
98         ];
99         rs = rq.post(httpbinUrl ~ "post", files);
100         assert(rs.code==200);
101         info("Check POST chunked from file.byChunk");
102         f = File(tmpfname, "rb");
103         rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream");
104         if (httpbinUrl != "http://httpbin.org/" ) {
105             assert(rs.code==200);
106             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
107             assert(data=="abcdefgh\n12345678\n");
108         }
109         f.close();
110     }
111     // ranges
112     {
113         info("Check POST chunked from lineSplitter");
114         auto s = lineSplitter("one,\ntwo,\nthree.");
115         rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream");
116         if (httpbinUrl != "http://httpbin.org/" ) {
117             assert(rs.code==200);
118             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
119             assert(data=="one,two,three.");
120         }
121     }
122     {
123         info("Check POST chunked from array");
124         auto s = ["one,", "two,", "three."];
125         rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream");
126         if (httpbinUrl != "http://httpbin.org/" ) {
127             assert(rs.code==200);
128             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
129             assert(data=="one,two,three.");
130         }
131     }
132     {
133         info("Check POST chunked using std.range.chunks()");
134         auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
135         rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
136         if (httpbinUrl != "http://httpbin.org/") {
137             assert(rs.code==200);
138             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
139             assert(data==s);
140         }
141     }
142     info("Check POST from QueryParams");
143     {
144         rs = rq.post(httpbinUrl ~ "post", queryParams("name[]", "first", "name[]", 2));
145         assert(rs.code==200);
146         auto data = parseJSON(cast(string)rs.responseBody).object["form"].object;
147         string[] a;
148         try {
149             a = to!(string[])(data["name[]"].str);
150         }
151         catch (JSONException e) {
152             a = data["name[]"].array.map!"a.str".array;
153         }
154         assert(equal(["first", "2"], a));
155     }
156     info("Check POST json");
157     {
158         rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"a b", "c":[1,2,3]}`, "application/json");
159         assert(rs.code==200);
160         auto j = parseJSON(cast(string)rs.responseBody).object["args"].object;
161         assert(j["b"].str == "x");
162         j = parseJSON(cast(string)rs.responseBody).object["json"].object;
163         assert(j["a"].str == "a b");
164         assert(j["c"].array.map!(a=>a.integer).array == [1,2,3]);
165     }
166     // associative array
167     info("Check POST from AA");
168     rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]);
169     assert(rs.code==200);
170     auto form = parseJSON(cast(string)rs.responseBody.data).object["form"].object;
171     assert(form["a"].str == "b ");
172     assert(form["c"].str == "d");
173     info("Check HEAD");
174     rs = rq.exec!"HEAD"(httpbinUrl);
175     assert(rs.code==200);
176     rs = rq.execute("HEAD", httpbinUrl);
177     assert(rs.code==200);
178     info("Check DELETE");
179     rs = rq.exec!"DELETE"(httpbinUrl ~ "delete");
180     assert(rs.code==200);
181     rs = rq.execute("DELETE", httpbinUrl ~ "delete");
182     assert(rs.code==200);
183     info("Check PUT");
184     rs = rq.exec!"PUT"(httpbinUrl ~ "put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
185     assert(rs.code==200);
186     rs = rq.execute("PUT", httpbinUrl ~ "put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
187     assert(rs.code==200);
188     info("Check PATCH");
189     rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream");
190     assert(rs.code==200);
191     rs = rq.execute("PATCH", httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream");
192     assert(rs.code==200);
193 
194     info("Check compressed content");
195     rq = Request();
196     rq.keepAlive = true;
197     rq.addHeaders(["X-Header": "test"]);
198     rs = rq.get(httpbinUrl ~ "gzip");
199     assert(rs.code==200);
200     info("gzip - ok");
201     rs = rq.get(httpbinUrl ~ "deflate");
202     assert(rs.code==200);
203     info("deflate - ok");
204     
205     info("Check cookie");
206     rq = Request();
207     rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef");
208     assert(rs.code == 200);
209     json = parseJSON(cast(string)rs.responseBody.data).object["cookies"].object;
210     assert(json["A"].str == "abcd");
211     assert(json["b"].str == "cdef");
212     foreach(c; rq.cookie._array) {
213         final switch(c.attr) {
214             case "A":
215                 assert(c.value == "abcd");
216                 break;
217             case "b":
218                 assert(c.value == "cdef");
219                 break;
220         }
221     }
222 
223     info("Check redirects");
224     rq = Request();
225     rq.keepAlive = true;
226     rs = rq.get(httpbinUrl ~ "relative-redirect/2");
227     assert((cast(HTTPResponse)rs).history.length == 2);
228     assert((cast(HTTPResponse)rs).code==200);
229 
230     rs = rq.get(httpbinUrl ~ "absolute-redirect/2");
231     assert((cast(HTTPResponse)rs).history.length == 2);
232     assert((cast(HTTPResponse)rs).code==200);
233     //    rq = Request();
234     info("Check maxredirects");
235     rq.maxRedirects = 2;
236     rq.keepAlive = false;
237     assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3"));
238 
239     rq.maxRedirects = 0;
240     rs = rq.get(httpbinUrl ~ "absolute-redirect/1");
241     assert(rs.code==302);
242     rq.maxRedirects = 10;
243 
244     info("Check chunked content");
245     rq = Request();
246     rq.keepAlive = true;
247     rq.bufferSize = 16*1024;
248     rs = rq.get(httpbinUrl ~ "range/1024");
249     assert(rs.code==200);
250     assert(rs.responseBody.length==1024);
251     
252     info("Check basic auth");
253     rq = Request();
254     rq.authenticator = new BasicAuthentication("user", "passwd");
255     rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd");
256     assert(rs.code==200);
257     
258     info("Check limits");
259     rq = Request();
260     rq.maxContentLength = 1;
261     assertThrown!RequestException(rq.get(httpbinUrl));
262     rq = Request();
263     rq.maxHeadersLength = 1;
264     assertThrown!RequestException(rq.get(httpbinUrl));
265 
266     info("Test getContent");
267     auto r = getContent(httpbinUrl ~ "stream/20");
268     assert(r.splitter('\n').filter!("a.length>0").count == 20);
269     r = getContent(httpbinUrl ~ "get", ["a":"b", "c":"d"]);
270     string name = "user", sex = "male";
271     immutable age = 42;
272     r = getContent(httpbinUrl ~ "get", "name", name, "age", age, "sex", sex);
273 
274     info("Test receiveAsRange with GET");
275     rq = Request();
276     rq.useStreaming = true;
277     rq.bufferSize = 16;
278     rs = rq.get(httpbinUrl ~ "stream/20");
279     auto stream = rs.receiveAsRange();
280     ubyte[] streamedContent;
281     while( !stream.empty() ) {
282         streamedContent ~= stream.front;
283         stream.popFront();
284     }
285     rq = Request();
286     rs = rq.get(httpbinUrl ~ "stream/20");
287     assert(streamedContent.length == rs.responseBody.data.length,
288             "streamedContent.length(%d) == rs.responseBody.data.length(%d)".
289             format(streamedContent.length, rs.responseBody.data.length));
290     info("Test postContent");
291     r = postContent(httpbinUrl ~ "post", `{"a":"b", "c":1}`, "application/json");
292     assert(parseJSON(cast(string)r).object["json"].object["c"].integer == 1);
293 
294     /// Posting to forms (for small data)
295     ///
296     /// posting query parameters using "application/x-www-form-urlencoded"
297     info("Test postContent using query params");
298     r = postContent(httpbinUrl ~ "post", queryParams("first", "a", "second", 2));
299     assert(parseJSON(cast(string)r).object["form"].object["first"].str == "a");
300     
301     /// posting using multipart/form-data (large data and files). See docs fot HTTPRequest
302     info("Test postContent form");
303     MultipartForm mpform;
304     mpform.add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
305     r = postContent(httpbinUrl ~ "post", mpform);
306     assert(parseJSON(cast(string)r).object["form"].object["greeting"].str == "hello");
307     
308     /// you can do this using Request struct to access response details
309     info("Test postContent form via Request()");
310     rq = Request();
311     mpform = MultipartForm().add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
312     rs = rq.post(httpbinUrl ~ "post", mpform);
313     assert(rs.code == 200);
314     assert(parseJSON(cast(string)(rs.responseBody().data)).object["form"].object["greeting"].str == "hello");
315 
316     info("Test receiveAsRange with POST");
317     streamedContent.length = 0;
318     rq = Request();
319     rq.useStreaming = true;
320     rq.bufferSize = 16;
321     string s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
322     rs = rq.post(httpbinUrl ~ "post", s.representation, "application/octet-stream");
323     stream = rs.receiveAsRange();
324     while( !stream.empty() ) {
325         streamedContent ~= stream.front;
326         stream.popFront();
327     }
328     rq = Request();
329     rs = rq.post(httpbinUrl ~ "post", s.representation, "application/octet-stream");
330     assert(streamedContent == rs.responseBody.data);
331 
332     streamedContent.length = 0;
333     rq = Request();
334     rq.useStreaming = true;
335     rq.bufferSize = 16;
336     mpform = MultipartForm().add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
337     rs = rq.post(httpbinUrl ~ "post", mpform);
338     stream = rs.receiveAsRange();
339     while( !stream.empty() ) {
340         streamedContent ~= stream.front;
341         stream.popFront();
342     }
343     info("Check POST files using multiPartForm");
344     {
345         /// This is example on usage files with MultipartForm data.
346         /// For this example we have to create files which will be sent.
347         import std.file;
348         import std.path;
349         /// preapare files
350         auto tmpd = tempDir();
351         auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt";
352         auto f = File(tmpfname1, "wb");
353         f.rawWrite("file1 content\n");
354         f.close();
355         auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt";
356         f = File(tmpfname2, "wb");
357         f.rawWrite("file2 content\n");
358         f.close();
359         ///
360         /// Ok, files ready.
361         /// Now we will prepare Form data
362         ///
363         File f1 = File(tmpfname1, "rb");
364         File f2 = File(tmpfname2, "rb");
365         scope(exit) {
366             f1.close();
367             f2.close();
368         }
369         ///
370         /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type
371         ///
372         MultipartForm mForm = MultipartForm().
373         add(formData("Field1", cast(ubyte[])"form field from memory")).
374         add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])).
375         add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])).
376         add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"]));
377         /// everything ready, send request
378         rs = rq.post(httpbinUrl ~ "post", mForm);
379     }
380 
381     rq = Request();
382     mpform = MultipartForm().add(formData(/* field name */ "greeting", /* content */ cast(ubyte[])"hello"));
383     rs = rq.post(httpbinUrl ~ "post", mpform);
384     assert(parseJSON(cast(string)(rs.responseBody().data)).object["form"].object["greeting"].str ==
385            parseJSON(cast(string)streamedContent).object["form"].object["greeting"].str);
386 
387 
388     info("Test POST'ing from Rank(2) range with user-provided Content-Length");
389     rq.addHeaders(["content-length": to!string(s.length)]);
390     rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
391     auto flat_content = parseJSON(cast(string)rs.responseBody().data).object["data"];
392     if ( flat_content.type == JSON_TYPE.STRING ) {
393         // httpbin.org returns string in "data"
394         assert(s == flat_content.str);
395     } else {
396         // internal httpbin server return array ob bytes
397         assert(s.representation == flat_content.array.map!(i => i.integer).array);
398     }
399 
400     info("Check exception handling, error messages and timeouts are OK");
401     rq.clearHeaders();
402     rq.timeout = 1.seconds;
403     assertThrown!TimeoutException(rq.get(httpbinUrl ~ "delay/3"));
404 
405     info("Test get in parallel");
406     {
407         import std.stdio;
408         import std.parallelism;
409         import std.algorithm;
410         import std.string;
411         import core.atomic;
412         
413         immutable auto urls = [
414             "stream/10",
415             "stream/20",
416             "stream/30",
417             "stream/40",
418             "stream/50",
419             "stream/60",
420             "stream/70",
421         ].map!(a => httpbinUrl ~ a).array.idup;
422         
423         defaultPoolThreads(4);
424         
425         shared short lines;
426         
427         foreach(url; parallel(urls)) {
428             atomicOp!"+="(lines, getContent(url).splitter("\n").count);
429         }
430         assert(lines == 287);
431         
432     }
433 }
434 ///
435 /// Create array of query params from args
436 ///
437 auto queryParams(A...)(A args) pure @safe nothrow {
438     QueryParam[] res;
439     static if ( args.length >= 2 ) {
440         res = [QueryParam(args[0].to!string, args[1].to!string)] ~ queryParams(args[2..$]);
441     }
442     return res;
443 }
444 /**
445  * Call GET, and return response content.
446  * This is the simplest case, when all you need is the response body and have no parameters.
447  * Returns:
448  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
449  */
450 public auto ref getContent(A...)(string url) {
451     auto rq = Request();
452     auto rs = rq.get(url);
453     return rs.responseBody;
454 }
455 /**
456  * Call GET, and return response content.
457  * args = string[string] fo query parameters.
458  * Returns:
459  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
460  */
461 public auto ref getContent(A...)(string url, string[string] args) {
462     auto rq = Request();
463     auto rs = rq.get(url, args);
464     return rs.responseBody;
465 }
466 /**
467  * Call GET, and return response content.
468  * args = QueryParam[] of parameters.
469  * Returns:
470  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
471  */
472 public auto ref getContent(A...)(string url, QueryParam[] args) {
473     auto rq = Request();
474     auto rs = rq.get(url, args);
475     return rs.responseBody;
476 }
477 /**
478  * Call GET, and return response content.
479  * args = variadic args to supply parameter names and values.
480  * Returns:
481  * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method.
482  */
483 public auto ref getContent(A...)(string url, A args) if (args.length > 1 && args.length % 2 == 0 ) {
484     return Request().
485             get(url, queryParams(args)).
486             responseBody;
487 }
488 
489 ///
490 /// Call post and return response content.
491 ///
492 public auto postContent(A...)(string url, A args) {
493     auto rq = Request();
494     auto rs = rq.post(url, args);
495     return rs.responseBody;
496 }
497 
498 ///
499 package unittest {
500     import std.json;
501     import std.string;
502     import std.stdio;
503     import std.range;
504     import std.process;
505 
506     globalLogLevel(LogLevel.info);
507     // while we have no internal ftp server we can run tests in non-reloable networking environment
508     immutable unreliable_network = environment.get("UNRELIABLENETWORK", "false") == "true";
509 
510     /// ftp upload from range
511     info("Test getContent(ftp)");
512     auto r = getContent("ftp://speedtest.tele2.net/1KB.zip");
513     assert(unreliable_network || r.length == 1024);
514 
515     info("Test receiveAsRange with GET(ftp)");
516     ubyte[] streamedContent;
517     auto rq = Request();
518     rq.useStreaming = true;
519     streamedContent.length = 0;
520     auto rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
521     auto stream = rs.receiveAsRange;
522     while( !stream.empty() ) {
523         streamedContent ~= stream.front;
524         stream.popFront();
525     }
526     assert(unreliable_network || streamedContent.length == 1024);
527     info("Test postContent ftp");
528     r = postContent("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
529     assert(unreliable_network || r.length == 0);
530 
531     //
532     info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
533     rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation);
534     assert(unreliable_network || rs.code == 226);
535     info("ftp get  ", "ftp://speedtest.tele2.net/nonexistent", ", in same session.");
536     rs = rq.get("ftp://speedtest.tele2.net/nonexistent");
537     assert(unreliable_network || rs.code != 226);
538     rq.useStreaming = false;
539     info("ftp get  ", "ftp://speedtest.tele2.net/1KB.zip", ", in same session.");
540     rs = rq.get("ftp://speedtest.tele2.net/1KB.zip");
541     assert(unreliable_network || rs.code == 226);
542     assert(unreliable_network || rs.responseBody.length == 1024);
543     info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT");
544     rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "another test, ignore please\n".representation);
545     assert(unreliable_network || rs.code == 226);
546     info("ftp get  ", "ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
547     try {
548         rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
549     } catch (ConnectError e)
550     {
551     }
552     assert(unreliable_network || rs.code == 226);
553     rq.authenticator = new BasicAuthentication("anonymous", "request@");
554     try {
555         rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT");
556     } catch (ConnectError e)
557     {
558     }
559     assert(unreliable_network || rs.code == 226);
560     info("testing ftp - done.");
561 }
562