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