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 8 import std.datetime; 9 import std.experimental.logger; 10 import requests.uri; 11 12 /*********************************** 13 * This is simplest interface to both http and ftp protocols. 14 * Request has methods get, post and exec which routed to proper concrete handler (http or ftp, etc). 15 * To enable some protocol-specific featutes you have to use protocol interface directly (see docs for HTTPRequest or FTPRequest) 16 */ 17 struct Request { 18 private { 19 URI _uri; 20 HTTPRequest _http; // route all http/https requests here 21 FTPRequest _ftp; // route all ftp requests here 22 } 23 /// Set timeout on IO operation. 24 /// $(B v) - timeout value 25 /// 26 @property void timeout(Duration v) pure @nogc nothrow { 27 _http.timeout = v; 28 _ftp.timeout = v; 29 } 30 /// Set http keepAlive value 31 /// $(B v) - use keepalive requests - $(B true), or not - $(B false) 32 @property void keepAlive(bool v) pure @nogc nothrow { 33 _http.keepAlive = v; 34 } 35 /// Set limit on HTTP redirects 36 /// $(B v) - limit on redirect depth 37 @property void maxRedirects(uint v) pure @nogc nothrow { 38 _http.maxRedirects = v; 39 } 40 /// Set maximum content lenth both for http and ftp requests 41 /// $(B v) - maximum content length in bytes. When limit reached - throw RequestException 42 @property void maxContentLength(size_t v) pure @nogc nothrow { 43 _http.maxContentLength = v; 44 _ftp.maxContentLength = v; 45 } 46 /// Set maximum length for HTTP headers 47 /// $(B v) - maximum length of the HTTP response. When limit reached - throw RequestException 48 @property void maxHeadersLength(size_t v) pure @nogc nothrow { 49 _http.maxHeadersLength = v; 50 } 51 /// Set IO buffer size for http and ftp requests 52 /// $(B v) - buffer size in bytes. 53 @property void bufferSize(size_t v) { 54 _http.bufferSize = v; 55 _ftp.bufferSize = v; 56 } 57 /// Set verbosity for HTTP or FTP requests. 58 /// $(B v) - verbosity level (0 - no output, 1 - headers to stdout, 2 - headers and body progress to stdout). default = 0. 59 @property void verbosity(uint v) { 60 _http.verbosity = v; 61 _ftp.verbosity = v; 62 } 63 /// Set authenticator for http requests. 64 /// $(B v) - Auth instance. 65 @property void authenticator(Auth v) { 66 _http.authenticator = v; 67 } 68 /// Execute GET for http and retrieve file for FTP. 69 /// You have to provide at least $(B uri). All other arguments should conform to HTTPRequest.get or FTPRequest.get depending on the URI scheme. 70 /// 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) 71 /// you will receive Exception("Operation not supported for ftp") 72 /// 73 Response get(A...)(string uri, A args) { 74 if ( uri ) { 75 _uri = URI(uri); 76 } 77 final switch ( _uri.scheme ) { 78 case "http", "https": 79 _http.uri = _uri; 80 static if (__traits(compiles, _http.get(null, args))) { 81 return _http.get(null, args); 82 } else { 83 throw new Exception("Operation not supported for http"); 84 } 85 case "ftp": 86 return _ftp.get(uri); 87 } 88 } 89 /// Execute POST for http and STOR file for FTP. 90 /// You have to provide $(B uri) and data. Data should conform to HTTPRequest.post or FTPRequest.post depending on the URI scheme. 91 /// When arguments do not conform scheme you will receive Exception("Operation not supported for ftp") 92 /// 93 Response post(A...)(string uri, A args) { 94 if ( uri ) { 95 _uri = URI(uri); 96 } 97 final switch ( _uri.scheme ) { 98 case "http", "https": 99 _http.uri = _uri; 100 static if (__traits(compiles, _http.post(null, args))) { 101 return _http.post(null, args); 102 } else { 103 throw new Exception("Operation not supported for http"); 104 } 105 case "ftp": 106 static if (__traits(compiles, _ftp.post(uri, args))) { 107 return _ftp.post(uri, args); 108 } else { 109 throw new Exception("Operation not supported for ftp"); 110 } 111 } 112 } 113 Response exec(string method="GET", A...)(A args) { 114 return _http.exec!(method)(args); 115 } 116 } 117 /// 118 unittest { 119 import std.algorithm; 120 import std.range; 121 import std.array; 122 import std.json; 123 import std.stdio; 124 import std.string; 125 import std.exception; 126 127 globalLogLevel(LogLevel.info); 128 129 infof("testing Request"); 130 Request rq; 131 Response rs; 132 // 133 rs = rq.get("https://httpbin.org/"); 134 assert(rs.code==200); 135 assert(rs.responseBody.length > 0); 136 rs = rq.get("http://httpbin.org/get", ["c":" d", "a":"b"]); 137 assert(rs.code == 200); 138 auto json = parseJSON(rs.responseBody.data).object["args"].object; 139 assert(json["c"].str == " d"); 140 assert(json["a"].str == "b"); 141 142 globalLogLevel(LogLevel.info); 143 rq = Request(); 144 rq.keepAlive = true; 145 // handmade json 146 info("Check POST json"); 147 rs = rq.post("http://httpbin.org/post?b=x", `{"a":"☺ ", "c":[1,2,3]}`, "application/json"); 148 assert(rs.code==200); 149 json = parseJSON(rs.responseBody.data).object["args"].object; 150 assert(json["b"].str == "x"); 151 json = parseJSON(rs.responseBody.data).object["json"].object; 152 assert(json["a"].str == "☺ "); 153 assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]); 154 { 155 import std.file; 156 import std.path; 157 auto tmpd = tempDir(); 158 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 159 auto f = File(tmpfname, "wb"); 160 f.rawWrite("abcdefgh\n12345678\n"); 161 f.close(); 162 // files 163 globalLogLevel(LogLevel.info); 164 info("Check POST files"); 165 PostFile[] files = [ 166 {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 167 {fileName: tmpfname} 168 ]; 169 rs = rq.post("http://httpbin.org/post", files); 170 assert(rs.code==200); 171 info("Check POST chunked from file.byChunk"); 172 f = File(tmpfname, "rb"); 173 rs = rq.post("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 174 assert(rs.code==200); 175 auto data = parseJSON(rs.responseBody.data).object["data"].str; 176 assert(data=="abcdefgh\n12345678\n"); 177 f.close(); 178 } 179 { 180 // string 181 info("Check POST utf8 string"); 182 rs = rq.post("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 183 assert(rs.code==200); 184 auto data = parseJSON(rs.responseBody.data).object["data"].str; 185 assert(data=="привiт, свiт!"); 186 } 187 // ranges 188 { 189 info("Check POST chunked from lineSplitter"); 190 auto s = lineSplitter("one,\ntwo,\nthree."); 191 rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 192 assert(rs.code==200); 193 auto data = parseJSON(rs.responseBody.toString).object["data"].str; 194 assert(data=="one,two,three."); 195 } 196 { 197 info("Check POST chunked from array"); 198 auto s = ["one,", "two,", "three."]; 199 rs = rq.post("http://httpbin.org/post", s, "application/octet-stream"); 200 assert(rs.code==200); 201 auto data = parseJSON(rs.responseBody.data).object["data"].str; 202 assert(data=="one,two,three."); 203 } 204 { 205 info("Check POST chunked using std.range.chunks()"); 206 auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 207 rs = rq.post("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 208 assert(rs.code==200); 209 auto data = parseJSON(rs.responseBody.data).object["data"].str; 210 assert(data==s); 211 } 212 // associative array 213 rs = rq.post("http://httpbin.org/post", ["a":"b ", "c":"d"]); 214 assert(rs.code==200); 215 auto form = parseJSON(rs.responseBody.data).object["form"].object; 216 assert(form["a"].str == "b "); 217 assert(form["c"].str == "d"); 218 info("Check HEAD"); 219 rs = rq.exec!"HEAD"("http://httpbin.org/"); 220 assert(rs.code==200); 221 info("Check DELETE"); 222 rs = rq.exec!"DELETE"("http://httpbin.org/delete"); 223 assert(rs.code==200); 224 info("Check PUT"); 225 rs = rq.exec!"PUT"("http://httpbin.org/put", `{"a":"b", "c":[1,2,3]}`, "application/json"); 226 assert(rs.code==200); 227 info("Check PATCH"); 228 rs = rq.exec!"PATCH"("http://httpbin.org/patch", "привiт, свiт!", "application/octet-stream"); 229 assert(rs.code==200); 230 231 info("Check compressed content"); 232 globalLogLevel(LogLevel.info); 233 rq = Request(); 234 rq.keepAlive = true; 235 rs = rq.get("http://httpbin.org/gzip"); 236 assert(rs.code==200); 237 info("gzip - ok"); 238 rs = rq.get("http://httpbin.org/deflate"); 239 assert(rs.code==200); 240 info("deflate - ok"); 241 242 info("Check redirects"); 243 globalLogLevel(LogLevel.info); 244 rq = Request(); 245 rq.keepAlive = true; 246 rs = rq.get("http://httpbin.org/relative-redirect/2"); 247 assert((cast(HTTPResponse)rs).history.length == 2); 248 assert((cast(HTTPResponse)rs).code==200); 249 // rq = Request(); 250 // rq.keepAlive = true; 251 // rq.proxy = "http://localhost:8888/"; 252 rs = rq.get("http://httpbin.org/absolute-redirect/2"); 253 assert((cast(HTTPResponse)rs).history.length == 2); 254 assert((cast(HTTPResponse)rs).code==200); 255 // rq = Request(); 256 rq.maxRedirects = 2; 257 rq.keepAlive = false; 258 rs = rq.get("https://httpbin.org/absolute-redirect/3"); 259 assert((cast(HTTPResponse)rs).history.length == 2); 260 assert((cast(HTTPResponse)rs).code==302); 261 262 info("Check utf8 content"); 263 globalLogLevel(LogLevel.info); 264 rq = Request(); 265 rs = rq.get("http://httpbin.org/encoding/utf8"); 266 assert(rs.code==200); 267 268 info("Check chunked content"); 269 globalLogLevel(LogLevel.info); 270 rq = Request(); 271 rq.keepAlive = true; 272 rq.bufferSize = 16*1024; 273 rs = rq.get("http://httpbin.org/range/1024"); 274 assert(rs.code==200); 275 assert(rs.responseBody.length==1024); 276 277 info("Check basic auth"); 278 globalLogLevel(LogLevel.info); 279 rq = Request(); 280 rq.authenticator = new BasicAuthentication("user", "passwd"); 281 rs = rq.get("http://httpbin.org/basic-auth/user/passwd"); 282 assert(rs.code==200); 283 284 globalLogLevel(LogLevel.info); 285 info("Check exception handling, error messages are OK"); 286 rq = Request(); 287 rq.timeout = 1.seconds; 288 assertThrown!TimeoutException(rq.get("http://httpbin.org/delay/3")); 289 assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/")); 290 assertThrown!ConnectError(rq.get("http://1.1.1.1/")); 291 //assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/")); 292 293 globalLogLevel(LogLevel.info); 294 info("Check limits"); 295 rq = Request(); 296 rq.maxContentLength = 1; 297 assertThrown!RequestException(rq.get("http://httpbin.org/")); 298 rq = Request(); 299 rq.maxHeadersLength = 1; 300 assertThrown!RequestException(rq.get("http://httpbin.org/")); 301 // 302 info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT"); 303 rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation); 304 assert(rs.code == 226); 305 info("ftp get ", "ftp://speedtest.tele2.net/nonexistent", ", in same session."); 306 rs = rq.get("ftp://speedtest.tele2.net/nonexistent"); 307 assert(rs.code != 226); 308 info("ftp get ", "ftp://speedtest.tele2.net/1KB.zip", ", in same session."); 309 rs = rq.get("ftp://speedtest.tele2.net/1KB.zip"); 310 assert(rs.code == 226); 311 assert(rs.responseBody.length == 1024); 312 info("ftp get ", "ftp://ftp.uni-bayreuth.de/README"); 313 rs = rq.get("ftp://ftp.uni-bayreuth.de/README"); 314 assert(rs.code == 226); 315 info("ftp post ", "ftp://speedtest.tele2.net/upload/TEST.TXT"); 316 rs = rq.post("ftp://speedtest.tele2.net/upload/TEST.TXT", "another test, ignore please\n".representation); 317 assert(rs.code == 226); 318 info("ftp get ", "ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT"); 319 rs = rq.get("ftp://ftp.iij.ad.jp/pub/FreeBSD/README.TXT"); 320 assert(rs.code == 226); 321 info("testing ftp - done."); 322 } 323 324 /** 325 * Call GET, and return response content. 326 * This is the simplest case, when all you need is the response body. 327 * Returns: 328 * Buffer!ubyte which you can use as ForwardRange or DirectAccessRange, or extract data with .data() method. 329 */ 330 public auto getContent(A...)(string url, A args) { 331 auto rq = Request(); 332 auto rs = rq.get(url, args); 333 return rs.responseBody; 334 } 335 /// 336 public unittest { 337 import std.algorithm; 338 globalLogLevel(LogLevel.info); 339 info("Test getContent"); 340 auto r = getContent("https://httpbin.org/stream/20"); 341 assert(r.splitter('\n').filter!("a.length>0").count == 20); 342 r = getContent("ftp://speedtest.tele2.net/1KB.zip"); 343 assert(r.length == 1024); 344 } 345 346 /** 347 * Call post and return response content. 348 */ 349 public auto postContent(A...)(string url, A args) { 350 auto rq = Request(); 351 auto rs = rq.post(url, args); 352 return rs.responseBody; 353 } 354 /// 355 public unittest { 356 import std.json; 357 import std.string; 358 globalLogLevel(LogLevel.info); 359 info("Test postContent"); 360 auto r = postContent("http://httpbin.org/post", `{"a":"b", "c":1}`, "application/json"); 361 assert(parseJSON(r.data).object["json"].object["c"].integer == 1); 362 r = postContent("ftp://speedtest.tele2.net/upload/TEST.TXT", "test, ignore please\n".representation); 363 assert(r.length == 0); 364 }