| // Written in the D programming language. | |
| /** | |
| Networking client functionality as provided by $(HTTP curl.haxx.se/libcurl, | |
| libcurl). The libcurl library must be installed on the system in order to use | |
| this module. | |
| $(SCRIPT inhibitQuickIndex = 1;) | |
| $(DIVC quickindex, | |
| $(BOOKTABLE , | |
| $(TR $(TH Category) $(TH Functions) | |
| ) | |
| $(TR $(TDNW High level) $(TD $(MYREF download) $(MYREF upload) $(MYREF get) | |
| $(MYREF post) $(MYREF put) $(MYREF del) $(MYREF options) $(MYREF trace) | |
| $(MYREF connect) $(MYREF byLine) $(MYREF byChunk) | |
| $(MYREF byLineAsync) $(MYREF byChunkAsync) ) | |
| ) | |
| $(TR $(TDNW Low level) $(TD $(MYREF HTTP) $(MYREF FTP) $(MYREF | |
| SMTP) ) | |
| ) | |
| ) | |
| ) | |
| Note: | |
| You may need to link with the $(B curl) library, e.g. by adding $(D "libs": ["curl"]) | |
| to your $(B dub.json) file if you are using $(LINK2 http://code.dlang.org, DUB). | |
| Windows x86 note: | |
| A DMD compatible libcurl static library can be downloaded from the dlang.org | |
| $(LINK2 https://downloads.dlang.org/other/index.html, download archive page). | |
| This module is not available for iOS, tvOS or watchOS. | |
| Compared to using libcurl directly, this module allows simpler client code for | |
| common uses, requires no unsafe operations, and integrates better with the rest | |
| of the language. Furthermore it provides $(MREF_ALTTEXT range, std,range) | |
| access to protocols supported by libcurl both synchronously and asynchronously. | |
| A high level and a low level API are available. The high level API is built | |
| entirely on top of the low level one. | |
| The high level API is for commonly used functionality such as HTTP/FTP get. The | |
| $(LREF byLineAsync) and $(LREF byChunkAsync) functions asynchronously | |
| perform the request given, outputting the fetched content into a $(MREF_ALTTEXT range, std,range). | |
| The low level API allows for streaming, setting request headers and cookies, and other advanced features. | |
| $(BOOKTABLE Cheat Sheet, | |
| $(TR $(TH Function Name) $(TH Description) | |
| ) | |
| $(LEADINGROW High level) | |
| $(TR $(TDNW $(LREF download)) $(TD $(D | |
| download("ftp.digitalmars.com/sieve.ds", "/tmp/downloaded-ftp-file")) | |
| downloads file from URL to file system.) | |
| ) | |
| $(TR $(TDNW $(LREF upload)) $(TD $(D | |
| upload("/tmp/downloaded-ftp-file", "ftp.digitalmars.com/sieve.ds");) | |
| uploads file from file system to URL.) | |
| ) | |
| $(TR $(TDNW $(LREF get)) $(TD $(D | |
| get("dlang.org")) returns a char[] containing the dlang.org web page.) | |
| ) | |
| $(TR $(TDNW $(LREF put)) $(TD $(D | |
| put("dlang.org", "Hi")) returns a char[] containing | |
| the dlang.org web page. after a HTTP PUT of "hi") | |
| ) | |
| $(TR $(TDNW $(LREF post)) $(TD $(D | |
| post("dlang.org", "Hi")) returns a char[] containing | |
| the dlang.org web page. after a HTTP POST of "hi") | |
| ) | |
| $(TR $(TDNW $(LREF byLine)) $(TD $(D | |
| byLine("dlang.org")) returns a range of char[] containing the | |
| dlang.org web page.) | |
| ) | |
| $(TR $(TDNW $(LREF byChunk)) $(TD $(D | |
| byChunk("dlang.org", 10)) returns a range of ubyte[10] containing the | |
| dlang.org web page.) | |
| ) | |
| $(TR $(TDNW $(LREF byLineAsync)) $(TD $(D | |
| byLineAsync("dlang.org")) asynchronously returns a range of char[] containing the dlang.org web | |
| page.) | |
| ) | |
| $(TR $(TDNW $(LREF byChunkAsync)) $(TD $(D | |
| byChunkAsync("dlang.org", 10)) asynchronously returns a range of ubyte[10] containing the | |
| dlang.org web page.) | |
| ) | |
| $(LEADINGROW Low level | |
| ) | |
| $(TR $(TDNW $(LREF HTTP)) $(TD Struct for advanced HTTP usage)) | |
| $(TR $(TDNW $(LREF FTP)) $(TD Struct for advanced FTP usage)) | |
| $(TR $(TDNW $(LREF SMTP)) $(TD Struct for advanced SMTP usage)) | |
| ) | |
| Example: | |
| --- | |
| import std.net.curl, std.stdio; | |
| // Return a char[] containing the content specified by a URL | |
| auto content = get("dlang.org"); | |
| // Post data and return a char[] containing the content specified by a URL | |
| auto content = post("mydomain.com/here.cgi", ["name1" : "value1", "name2" : "value2"]); | |
| // Get content of file from ftp server | |
| auto content = get("ftp.digitalmars.com/sieve.ds"); | |
| // Post and print out content line by line. The request is done in another thread. | |
| foreach (line; byLineAsync("dlang.org", "Post data")) | |
| writeln(line); | |
| // Get using a line range and proxy settings | |
| auto client = HTTP(); | |
| client.proxy = "1.2.3.4"; | |
| foreach (line; byLine("dlang.org", client)) | |
| writeln(line); | |
| --- | |
| For more control than the high level functions provide, use the low level API: | |
| Example: | |
| --- | |
| import std.net.curl, std.stdio; | |
| // GET with custom data receivers | |
| auto http = HTTP("dlang.org"); | |
| http.onReceiveHeader = | |
| (in char[] key, in char[] value) { writeln(key, ": ", value); }; | |
| http.onReceive = (ubyte[] data) { /+ drop +/ return data.length; }; | |
| http.perform(); | |
| --- | |
| First, an instance of the reference-counted HTTP struct is created. Then the | |
| custom delegates are set. These will be called whenever the HTTP instance | |
| receives a header and a data buffer, respectively. In this simple example, the | |
| headers are written to stdout and the data is ignored. If the request is | |
| stopped before it has finished then return something less than data.length from | |
| the onReceive callback. See $(LREF onReceiveHeader)/$(LREF onReceive) for more | |
| information. Finally, the HTTP request is performed by calling perform(), which is | |
| synchronous. | |
| Source: $(PHOBOSSRC std/net/curl.d) | |
| Copyright: Copyright Jonas Drewsen 2011-2012 | |
| License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). | |
| Authors: Jonas Drewsen. Some of the SMTP code contributed by Jimmy Cao. | |
| Credits: The functionality is based on $(HTTP curl.haxx.se/libcurl, libcurl). | |
| libcurl is licensed under an MIT/X derivative license. | |
| */ | |
| /* | |
| Copyright Jonas Drewsen 2011 - 2012. | |
| Distributed under the Boost Software License, Version 1.0. | |
| (See accompanying file LICENSE_1_0.txt or copy at | |
| http://www.boost.org/LICENSE_1_0.txt) | |
| */ | |
| module std.net.curl; | |
| public import etc.c.curl : CurlOption; | |
| import core.time : dur; | |
| import etc.c.curl : CURLcode; | |
| import std.range.primitives; | |
| import std.encoding : EncodingScheme; | |
| import std.traits : isSomeChar; | |
| import std.typecons : Flag, Yes, No, Tuple; | |
| version (iOS) | |
| version = iOSDerived; | |
| else version (TVOS) | |
| version = iOSDerived; | |
| else version (WatchOS) | |
| version = iOSDerived; | |
| version (iOSDerived) {} | |
| else: | |
| version (StdUnittest) | |
| { | |
| import std.socket : Socket, SocketShutdown; | |
| private struct TestServer | |
| { | |
| import std.concurrency : Tid; | |
| import std.socket : Socket, TcpSocket; | |
| string addr() { return _addr; } | |
| void handle(void function(Socket s) dg) | |
| { | |
| import std.concurrency : send; | |
| tid.send(dg); | |
| } | |
| private: | |
| string _addr; | |
| Tid tid; | |
| TcpSocket sock; | |
| static void loop(shared TcpSocket listener) | |
| { | |
| import std.concurrency : OwnerTerminated, receiveOnly; | |
| import std.stdio : stderr; | |
| try while (true) | |
| { | |
| void function(Socket) handler = void; | |
| try | |
| handler = receiveOnly!(typeof(handler)); | |
| catch (OwnerTerminated) | |
| return; | |
| handler((cast() listener).accept); | |
| } | |
| catch (Throwable e) | |
| { | |
| // https://issues.dlang.org/show_bug.cgi?id=7018 | |
| stderr.writeln(e); | |
| } | |
| } | |
| } | |
| private TestServer startServer() | |
| { | |
| import std.concurrency : spawn; | |
| import std.socket : INADDR_LOOPBACK, InternetAddress, TcpSocket; | |
| tlsInit = true; | |
| auto sock = new TcpSocket; | |
| sock.bind(new InternetAddress(INADDR_LOOPBACK, InternetAddress.PORT_ANY)); | |
| sock.listen(1); | |
| auto addr = sock.localAddress.toString(); | |
| auto tid = spawn(&TestServer.loop, cast(shared) sock); | |
| return TestServer(addr, tid, sock); | |
| } | |
| /** Test server */ | |
| __gshared TestServer server; | |
| /** Thread-local storage init */ | |
| bool tlsInit; | |
| private ref TestServer testServer() | |
| { | |
| import std.concurrency : initOnce; | |
| return initOnce!server(startServer()); | |
| } | |
| static ~this() | |
| { | |
| // terminate server from a thread local dtor of the thread that started it, | |
| // because thread_joinall is called before shared module dtors | |
| if (tlsInit && server.sock) | |
| { | |
| server.sock.shutdown(SocketShutdown.RECEIVE); | |
| server.sock.close(); | |
| } | |
| } | |
| private struct Request(T) | |
| { | |
| string hdrs; | |
| immutable(T)[] bdy; | |
| } | |
| private Request!T recvReq(T=char)(Socket s) | |
| { | |
| import std.algorithm.comparison : min; | |
| import std.algorithm.searching : find, canFind; | |
| import std.conv : to; | |
| import std.regex : ctRegex, matchFirst; | |
| ubyte[1024] tmp=void; | |
| ubyte[] buf; | |
| while (true) | |
| { | |
| auto nbytes = s.receive(tmp[]); | |
| assert(nbytes >= 0); | |
| immutable beg = buf.length > 3 ? buf.length - 3 : 0; | |
| buf ~= tmp[0 .. nbytes]; | |
| auto bdy = buf[beg .. $].find(cast(ubyte[])"\r\n\r\n"); | |
| if (bdy.empty) | |
| continue; | |
| auto hdrs = cast(string) buf[0 .. $ - bdy.length]; | |
| bdy.popFrontN(4); | |
| // no support for chunked transfer-encoding | |
| if (auto m = hdrs.matchFirst(ctRegex!(`Content-Length: ([0-9]+)`, "i"))) | |
| { | |
| import std.uni : asUpperCase; | |
| if (hdrs.asUpperCase.canFind("EXPECT: 100-CONTINUE")) | |
| s.send(httpContinue); | |
| size_t remain = m.captures[1].to!size_t - bdy.length; | |
| while (remain) | |
| { | |
| nbytes = s.receive(tmp[0 .. min(remain, $)]); | |
| assert(nbytes >= 0); | |
| buf ~= tmp[0 .. nbytes]; | |
| remain -= nbytes; | |
| } | |
| } | |
| else | |
| { | |
| assert(bdy.empty); | |
| } | |
| bdy = buf[hdrs.length + 4 .. $]; | |
| return typeof(return)(hdrs, cast(immutable(T)[])bdy); | |
| } | |
| } | |
| private string httpOK(string msg) | |
| { | |
| import std.conv : to; | |
| return "HTTP/1.1 200 OK\r\n"~ | |
| "Content-Type: text/plain\r\n"~ | |
| "Content-Length: "~msg.length.to!string~"\r\n"~ | |
| "\r\n"~ | |
| msg; | |
| } | |
| private string httpOK() | |
| { | |
| return "HTTP/1.1 200 OK\r\n"~ | |
| "Content-Length: 0\r\n"~ | |
| "\r\n"; | |
| } | |
| private string httpNotFound() | |
| { | |
| return "HTTP/1.1 404 Not Found\r\n"~ | |
| "Content-Length: 0\r\n"~ | |
| "\r\n"; | |
| } | |
| private enum httpContinue = "HTTP/1.1 100 Continue\r\n\r\n"; | |
| } | |
| version (StdDdoc) import std.stdio; | |
| // Default data timeout for Protocols | |
| private enum _defaultDataTimeout = dur!"minutes"(2); | |
| /** | |
| Macros: | |
| CALLBACK_PARAMS = $(TABLE , | |
| $(DDOC_PARAM_ROW | |
| $(DDOC_PARAM_ID $(DDOC_PARAM dlTotal)) | |
| $(DDOC_PARAM_DESC total bytes to download) | |
| ) | |
| $(DDOC_PARAM_ROW | |
| $(DDOC_PARAM_ID $(DDOC_PARAM dlNow)) | |
| $(DDOC_PARAM_DESC currently downloaded bytes) | |
| ) | |
| $(DDOC_PARAM_ROW | |
| $(DDOC_PARAM_ID $(DDOC_PARAM ulTotal)) | |
| $(DDOC_PARAM_DESC total bytes to upload) | |
| ) | |
| $(DDOC_PARAM_ROW | |
| $(DDOC_PARAM_ID $(DDOC_PARAM ulNow)) | |
| $(DDOC_PARAM_DESC currently uploaded bytes) | |
| ) | |
| ) | |
| */ | |
| /** Connection type used when the URL should be used to auto detect the protocol. | |
| * | |
| * This struct is used as placeholder for the connection parameter when calling | |
| * the high level API and the connection type (HTTP/FTP) should be guessed by | |
| * inspecting the URL parameter. | |
| * | |
| * The rules for guessing the protocol are: | |
| * 1, if URL starts with ftp://, ftps:// or ftp. then FTP connection is assumed. | |
| * 2, HTTP connection otherwise. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * // Two requests below will do the same. | |
| * char[] content; | |
| * | |
| * // Explicit connection provided | |
| * content = get!HTTP("dlang.org"); | |
| * | |
| * // Guess connection type by looking at the URL | |
| * content = get!AutoProtocol("ftp://foo.com/file"); | |
| * // and since AutoProtocol is default this is the same as | |
| * content = get("ftp://foo.com/file"); | |
| * // and will end up detecting FTP from the url and be the same as | |
| * content = get!FTP("ftp://foo.com/file"); | |
| * --- | |
| */ | |
| struct AutoProtocol { } | |
| // Returns true if the url points to an FTP resource | |
| private bool isFTPUrl(const(char)[] url) | |
| { | |
| import std.algorithm.searching : startsWith; | |
| import std.uni : toLower; | |
| return startsWith(url.toLower(), "ftp://", "ftps://", "ftp.") != 0; | |
| } | |
| // Is true if the Conn type is a valid Curl Connection type. | |
| private template isCurlConn(Conn) | |
| { | |
| enum auto isCurlConn = is(Conn : HTTP) || | |
| is(Conn : FTP) || is(Conn : AutoProtocol); | |
| } | |
| /** HTTP/FTP download to local file system. | |
| * | |
| * Params: | |
| * url = resource to download | |
| * saveToPath = path to store the downloaded content on local disk | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * download("https://httpbin.org/get", "/tmp/downloaded-http-file"); | |
| * ---- | |
| */ | |
| void download(Conn = AutoProtocol)(const(char)[] url, string saveToPath, Conn conn = Conn()) | |
| if (isCurlConn!Conn) | |
| { | |
| static if (is(Conn : HTTP) || is(Conn : FTP)) | |
| { | |
| import std.stdio : File; | |
| conn.url = url; | |
| auto f = File(saveToPath, "wb"); | |
| conn.onReceive = (ubyte[] data) { f.rawWrite(data); return data.length; }; | |
| conn.perform(); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return download!FTP(url, saveToPath, FTP()); | |
| else | |
| return download!HTTP(url, saveToPath, HTTP()); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| static import std.file; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| assert(s.recvReq.hdrs.canFind("GET /")); | |
| s.send(httpOK("Hello world")); | |
| }); | |
| auto fn = std.file.deleteme; | |
| scope (exit) | |
| { | |
| if (std.file.exists(fn)) | |
| std.file.remove(fn); | |
| } | |
| download(host, fn); | |
| assert(std.file.readText(fn) == "Hello world"); | |
| } | |
| } | |
| /** Upload file from local files system using the HTTP or FTP protocol. | |
| * | |
| * Params: | |
| * loadFromPath = path load data from local disk. | |
| * url = resource to upload to | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * upload("/tmp/downloaded-ftp-file", "ftp.digitalmars.com/sieve.ds"); | |
| * upload("/tmp/downloaded-http-file", "https://httpbin.org/post"); | |
| * ---- | |
| */ | |
| void upload(Conn = AutoProtocol)(string loadFromPath, const(char)[] url, Conn conn = Conn()) | |
| if (isCurlConn!Conn) | |
| { | |
| static if (is(Conn : HTTP)) | |
| { | |
| conn.url = url; | |
| conn.method = HTTP.Method.put; | |
| } | |
| else static if (is(Conn : FTP)) | |
| { | |
| conn.url = url; | |
| conn.handle.set(CurlOption.upload, 1L); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return upload!FTP(loadFromPath, url, FTP()); | |
| else | |
| return upload!HTTP(loadFromPath, url, HTTP()); | |
| } | |
| static if (is(Conn : HTTP) || is(Conn : FTP)) | |
| { | |
| import std.stdio : File; | |
| auto f = File(loadFromPath, "rb"); | |
| conn.onSend = buf => f.rawRead(buf).length; | |
| immutable sz = f.size; | |
| if (sz != ulong.max) | |
| conn.contentLength = sz; | |
| conn.perform(); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| static import std.file; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| auto fn = std.file.deleteme; | |
| scope (exit) | |
| { | |
| if (std.file.exists(fn)) | |
| std.file.remove(fn); | |
| } | |
| std.file.write(fn, "upload data\n"); | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("PUT /path")); | |
| assert(req.bdy.canFind("upload data")); | |
| s.send(httpOK()); | |
| }); | |
| upload(fn, host ~ "/path"); | |
| } | |
| } | |
| /** HTTP/FTP get content. | |
| * | |
| * Params: | |
| * url = resource to get | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. If asking | |
| * for `char`, content will be converted from the connection character set | |
| * (specified in HTTP response headers or FTP connection properties, both ISO-8859-1 | |
| * by default) to UTF-8. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * auto content = get("https://httpbin.org/get"); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the content of the resource pointed to by the URL. | |
| * | |
| * Throws: | |
| * | |
| * `CurlException` on error. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] get(Conn = AutoProtocol, T = char)(const(char)[] url, Conn conn = Conn()) | |
| if ( isCurlConn!Conn && (is(T == char) || is(T == ubyte)) ) | |
| { | |
| static if (is(Conn : HTTP)) | |
| { | |
| conn.method = HTTP.Method.get; | |
| return _basicHTTP!(T)(url, "", conn); | |
| } | |
| else static if (is(Conn : FTP)) | |
| { | |
| return _basicFTP!(T)(url, "", conn); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return get!(FTP,T)(url, FTP()); | |
| else | |
| return get!(HTTP,T)(url, HTTP()); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| assert(s.recvReq.hdrs.canFind("GET /path")); | |
| s.send(httpOK("GETRESPONSE")); | |
| }); | |
| auto res = get(host ~ "/path"); | |
| assert(res == "GETRESPONSE"); | |
| } | |
| } | |
| /** HTTP post content. | |
| * | |
| * Params: | |
| * url = resource to post to | |
| * postDict = data to send as the body of the request. An associative array | |
| * of `string` is accepted and will be encoded using | |
| * www-form-urlencoding | |
| * postData = data to send as the body of the request. An array | |
| * of an arbitrary type is accepted and will be cast to ubyte[] | |
| * before sending it. | |
| * conn = HTTP connection to use | |
| * T = The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. If asking | |
| * for `char`, content will be converted from the connection character set | |
| * (specified in HTTP response headers or FTP connection properties, both ISO-8859-1 | |
| * by default) to UTF-8. | |
| * | |
| * Examples: | |
| * ---- | |
| * import std.net.curl; | |
| * | |
| * auto content1 = post("https://httpbin.org/post", ["name1" : "value1", "name2" : "value2"]); | |
| * auto content2 = post("https://httpbin.org/post", [1,2,3,4]); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the content of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] post(T = char, PostUnit)(const(char)[] url, const(PostUnit)[] postData, HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| conn.method = HTTP.Method.post; | |
| return _basicHTTP!(T)(url, postData, conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("POST /path")); | |
| assert(req.bdy.canFind("POSTBODY")); | |
| s.send(httpOK("POSTRESPONSE")); | |
| }); | |
| auto res = post(host ~ "/path", "POSTBODY"); | |
| assert(res == "POSTRESPONSE"); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| auto data = new ubyte[](256); | |
| foreach (i, ref ub; data) | |
| ub = cast(ubyte) i; | |
| testServer.handle((s) { | |
| auto req = s.recvReq!ubyte; | |
| assert(req.bdy.canFind(cast(ubyte[])[0, 1, 2, 3, 4])); | |
| assert(req.bdy.canFind(cast(ubyte[])[253, 254, 255])); | |
| s.send(httpOK(cast(ubyte[])[17, 27, 35, 41])); | |
| }); | |
| auto res = post!ubyte(testServer.addr, data); | |
| assert(res == cast(ubyte[])[17, 27, 35, 41]); | |
| } | |
| /// ditto | |
| T[] post(T = char)(const(char)[] url, string[string] postDict, HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| import std.uri : urlEncode; | |
| return post!T(url, urlEncode(postDict), conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| import std.meta : AliasSeq; | |
| static immutable expected = ["name1=value1&name2=value2", "name2=value2&name1=value1"]; | |
| foreach (host; [testServer.addr, "http://" ~ testServer.addr]) | |
| { | |
| foreach (T; AliasSeq!(char, ubyte)) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq!char; | |
| s.send(httpOK(req.bdy)); | |
| }); | |
| auto res = post!T(host ~ "/path", ["name1" : "value1", "name2" : "value2"]); | |
| assert(canFind(expected, res)); | |
| } | |
| } | |
| } | |
| /** HTTP/FTP put content. | |
| * | |
| * Params: | |
| * url = resource to put | |
| * putData = data to send as the body of the request. An array | |
| * of an arbitrary type is accepted and will be cast to ubyte[] | |
| * before sending it. | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. If asking | |
| * for `char`, content will be converted from the connection character set | |
| * (specified in HTTP response headers or FTP connection properties, both ISO-8859-1 | |
| * by default) to UTF-8. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * auto content = put("https://httpbin.org/put", | |
| * "Putting this data"); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the content of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] put(Conn = AutoProtocol, T = char, PutUnit)(const(char)[] url, const(PutUnit)[] putData, | |
| Conn conn = Conn()) | |
| if ( isCurlConn!Conn && (is(T == char) || is(T == ubyte)) ) | |
| { | |
| static if (is(Conn : HTTP)) | |
| { | |
| conn.method = HTTP.Method.put; | |
| return _basicHTTP!(T)(url, putData, conn); | |
| } | |
| else static if (is(Conn : FTP)) | |
| { | |
| return _basicFTP!(T)(url, putData, conn); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return put!(FTP,T)(url, putData, FTP()); | |
| else | |
| return put!(HTTP,T)(url, putData, HTTP()); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("PUT /path")); | |
| assert(req.bdy.canFind("PUTBODY")); | |
| s.send(httpOK("PUTRESPONSE")); | |
| }); | |
| auto res = put(host ~ "/path", "PUTBODY"); | |
| assert(res == "PUTRESPONSE"); | |
| } | |
| } | |
| /** HTTP/FTP delete content. | |
| * | |
| * Params: | |
| * url = resource to delete | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * del("https://httpbin.org/delete"); | |
| * ---- | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| void del(Conn = AutoProtocol)(const(char)[] url, Conn conn = Conn()) | |
| if (isCurlConn!Conn) | |
| { | |
| static if (is(Conn : HTTP)) | |
| { | |
| conn.method = HTTP.Method.del; | |
| _basicHTTP!char(url, cast(void[]) null, conn); | |
| } | |
| else static if (is(Conn : FTP)) | |
| { | |
| import std.algorithm.searching : findSplitAfter; | |
| import std.conv : text; | |
| import std.exception : enforce; | |
| auto trimmed = url.findSplitAfter("ftp://")[1]; | |
| auto t = trimmed.findSplitAfter("/"); | |
| enum minDomainNameLength = 3; | |
| enforce!CurlException(t[0].length > minDomainNameLength, | |
| text("Invalid FTP URL for delete ", url)); | |
| conn.url = t[0]; | |
| enforce!CurlException(!t[1].empty, | |
| text("No filename specified to delete for URL ", url)); | |
| conn.addCommand("DELE " ~ t[1]); | |
| conn.perform(); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return del!FTP(url, FTP()); | |
| else | |
| return del!HTTP(url, HTTP()); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("DELETE /path")); | |
| s.send(httpOK()); | |
| }); | |
| del(host ~ "/path"); | |
| } | |
| } | |
| /** HTTP options request. | |
| * | |
| * Params: | |
| * url = resource make a option call to | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * auto http = HTTP(); | |
| * options("https://httpbin.org/headers", http); | |
| * writeln("Allow set to " ~ http.responseHeaders["Allow"]); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the options of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] options(T = char)(const(char)[] url, HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| conn.method = HTTP.Method.options; | |
| return _basicHTTP!(T)(url, null, conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("OPTIONS /path")); | |
| s.send(httpOK("OPTIONSRESPONSE")); | |
| }); | |
| auto res = options(testServer.addr ~ "/path"); | |
| assert(res == "OPTIONSRESPONSE"); | |
| } | |
| /** HTTP trace request. | |
| * | |
| * Params: | |
| * url = resource make a trace call to | |
| * conn = connection to use e.g. FTP or HTTP. The default AutoProtocol will | |
| * guess connection type and create a new instance for this call only. | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * trace("https://httpbin.org/headers"); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the trace info of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] trace(T = char)(const(char)[] url, HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| conn.method = HTTP.Method.trace; | |
| return _basicHTTP!(T)(url, cast(void[]) null, conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("TRACE /path")); | |
| s.send(httpOK("TRACERESPONSE")); | |
| }); | |
| auto res = trace(testServer.addr ~ "/path"); | |
| assert(res == "TRACERESPONSE"); | |
| } | |
| /** HTTP connect request. | |
| * | |
| * Params: | |
| * url = resource make a connect to | |
| * conn = HTTP connection to use | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * connect("https://httpbin.org/headers"); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the connect info of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] connect(T = char)(const(char)[] url, HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| conn.method = HTTP.Method.connect; | |
| return _basicHTTP!(T)(url, cast(void[]) null, conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("CONNECT /path")); | |
| s.send(httpOK("CONNECTRESPONSE")); | |
| }); | |
| auto res = connect(testServer.addr ~ "/path"); | |
| assert(res == "CONNECTRESPONSE"); | |
| } | |
| /** HTTP patch content. | |
| * | |
| * Params: | |
| * url = resource to patch | |
| * patchData = data to send as the body of the request. An array | |
| * of an arbitrary type is accepted and will be cast to ubyte[] | |
| * before sending it. | |
| * conn = HTTP connection to use | |
| * | |
| * The template parameter `T` specifies the type to return. Possible values | |
| * are `char` and `ubyte` to return `char[]` or `ubyte[]`. | |
| * | |
| * Example: | |
| * ---- | |
| * auto http = HTTP(); | |
| * http.addRequestHeader("Content-Type", "application/json"); | |
| * auto content = patch("https://httpbin.org/patch", `{"title": "Patched Title"}`, http); | |
| * ---- | |
| * | |
| * Returns: | |
| * A T[] range containing the content of the resource pointed to by the URL. | |
| * | |
| * See_Also: $(LREF HTTP.Method) | |
| */ | |
| T[] patch(T = char, PatchUnit)(const(char)[] url, const(PatchUnit)[] patchData, | |
| HTTP conn = HTTP()) | |
| if (is(T == char) || is(T == ubyte)) | |
| { | |
| conn.method = HTTP.Method.patch; | |
| return _basicHTTP!(T)(url, patchData, conn); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("PATCH /path")); | |
| assert(req.bdy.canFind("PATCHBODY")); | |
| s.send(httpOK("PATCHRESPONSE")); | |
| }); | |
| auto res = patch(testServer.addr ~ "/path", "PATCHBODY"); | |
| assert(res == "PATCHRESPONSE"); | |
| } | |
| /* | |
| * Helper function for the high level interface. | |
| * | |
| * It performs an HTTP request using the client which must have | |
| * been setup correctly before calling this function. | |
| */ | |
| private auto _basicHTTP(T)(const(char)[] url, const(void)[] sendData, HTTP client) | |
| { | |
| import std.algorithm.comparison : min; | |
| import std.format : format; | |
| import std.exception : enforce; | |
| import etc.c.curl : CurlSeek, CurlSeekPos; | |
| immutable doSend = sendData !is null && | |
| (client.method == HTTP.Method.post || | |
| client.method == HTTP.Method.put || | |
| client.method == HTTP.Method.patch); | |
| scope (exit) | |
| { | |
| client.onReceiveHeader = null; | |
| client.onReceiveStatusLine = null; | |
| client.onReceive = null; | |
| if (doSend) | |
| { | |
| client.onSend = null; | |
| client.handle.onSeek = null; | |
| client.contentLength = 0; | |
| } | |
| } | |
| client.url = url; | |
| HTTP.StatusLine statusLine; | |
| import std.array : appender; | |
| auto content = appender!(ubyte[])(); | |
| client.onReceive = (ubyte[] data) | |
| { | |
| content ~= data; | |
| return data.length; | |
| }; | |
| if (doSend) | |
| { | |
| client.contentLength = sendData.length; | |
| auto remainingData = sendData; | |
| client.onSend = delegate size_t(void[] buf) | |
| { | |
| size_t minLen = min(buf.length, remainingData.length); | |
| if (minLen == 0) return 0; | |
| buf[0 .. minLen] = cast(void[]) remainingData[0 .. minLen]; | |
| remainingData = remainingData[minLen..$]; | |
| return minLen; | |
| }; | |
| client.handle.onSeek = delegate(long offset, CurlSeekPos mode) | |
| { | |
| switch (mode) | |
| { | |
| case CurlSeekPos.set: | |
| remainingData = sendData[cast(size_t) offset..$]; | |
| return CurlSeek.ok; | |
| default: | |
| // As of curl 7.18.0, libcurl will not pass | |
| // anything other than CurlSeekPos.set. | |
| return CurlSeek.cantseek; | |
| } | |
| }; | |
| } | |
| client.onReceiveHeader = (in char[] key, | |
| in char[] value) | |
| { | |
| if (key == "content-length") | |
| { | |
| import std.conv : to; | |
| content.reserve(value.to!size_t); | |
| } | |
| }; | |
| client.onReceiveStatusLine = (HTTP.StatusLine l) { statusLine = l; }; | |
| client.perform(); | |
| enforce(statusLine.code / 100 == 2, new HTTPStatusException(statusLine.code, | |
| format("HTTP request returned status code %d (%s)", statusLine.code, statusLine.reason))); | |
| return _decodeContent!T(content.data, client.p.charset); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| import std.exception : collectException; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("GET /path")); | |
| s.send(httpNotFound()); | |
| }); | |
| auto e = collectException!HTTPStatusException(get(testServer.addr ~ "/path")); | |
| assert(e.msg == "HTTP request returned status code 404 (Not Found)"); | |
| assert(e.status == 404); | |
| } | |
| // Content length must be reset after post | |
| // https://issues.dlang.org/show_bug.cgi?id=14760 | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("POST /")); | |
| assert(req.bdy.canFind("POSTBODY")); | |
| s.send(httpOK("POSTRESPONSE")); | |
| req = s.recvReq; | |
| assert(req.hdrs.canFind("TRACE /")); | |
| assert(req.bdy.empty); | |
| s.blocking = false; | |
| ubyte[6] buf = void; | |
| assert(s.receive(buf[]) < 0); | |
| s.send(httpOK("TRACERESPONSE")); | |
| }); | |
| auto http = HTTP(); | |
| auto res = post(testServer.addr, "POSTBODY", http); | |
| assert(res == "POSTRESPONSE"); | |
| res = trace(testServer.addr, http); | |
| assert(res == "TRACERESPONSE"); | |
| } | |
| @system unittest // charset detection and transcoding to T | |
| { | |
| testServer.handle((s) { | |
| s.send("HTTP/1.1 200 OK\r\n"~ | |
| "Content-Length: 4\r\n"~ | |
| "Content-Type: text/plain; charset=utf-8\r\n" ~ | |
| "\r\n" ~ | |
| "äbc"); | |
| }); | |
| auto client = HTTP(); | |
| auto result = _basicHTTP!char(testServer.addr, "", client); | |
| assert(result == "äbc"); | |
| testServer.handle((s) { | |
| s.send("HTTP/1.1 200 OK\r\n"~ | |
| "Content-Length: 3\r\n"~ | |
| "Content-Type: text/plain; charset=iso-8859-1\r\n" ~ | |
| "\r\n" ~ | |
| 0xE4 ~ "bc"); | |
| }); | |
| client = HTTP(); | |
| result = _basicHTTP!char(testServer.addr, "", client); | |
| assert(result == "äbc"); | |
| } | |
| /* | |
| * Helper function for the high level interface. | |
| * | |
| * It performs an FTP request using the client which must have | |
| * been setup correctly before calling this function. | |
| */ | |
| private auto _basicFTP(T)(const(char)[] url, const(void)[] sendData, FTP client) | |
| { | |
| import std.algorithm.comparison : min; | |
| scope (exit) | |
| { | |
| client.onReceive = null; | |
| if (!sendData.empty) | |
| client.onSend = null; | |
| } | |
| ubyte[] content; | |
| if (client.encoding.empty) | |
| client.encoding = "ISO-8859-1"; | |
| client.url = url; | |
| client.onReceive = (ubyte[] data) | |
| { | |
| content ~= data; | |
| return data.length; | |
| }; | |
| if (!sendData.empty) | |
| { | |
| client.handle.set(CurlOption.upload, 1L); | |
| client.onSend = delegate size_t(void[] buf) | |
| { | |
| size_t minLen = min(buf.length, sendData.length); | |
| if (minLen == 0) return 0; | |
| buf[0 .. minLen] = cast(void[]) sendData[0 .. minLen]; | |
| sendData = sendData[minLen..$]; | |
| return minLen; | |
| }; | |
| } | |
| client.perform(); | |
| return _decodeContent!T(content, client.encoding); | |
| } | |
| /* Used by _basicHTTP() and _basicFTP() to decode ubyte[] to | |
| * correct string format | |
| */ | |
| private auto _decodeContent(T)(ubyte[] content, string encoding) | |
| { | |
| static if (is(T == ubyte)) | |
| { | |
| return content; | |
| } | |
| else | |
| { | |
| import std.exception : enforce; | |
| import std.format : format; | |
| // Optimally just return the utf8 encoded content | |
| if (encoding == "UTF-8") | |
| return cast(char[])(content); | |
| // The content has to be re-encoded to utf8 | |
| auto scheme = EncodingScheme.create(encoding); | |
| enforce!CurlException(scheme !is null, | |
| format("Unknown encoding '%s'", encoding)); | |
| auto strInfo = decodeString(content, scheme); | |
| enforce!CurlException(strInfo[0] != size_t.max, | |
| format("Invalid encoding sequence for encoding '%s'", | |
| encoding)); | |
| return strInfo[1]; | |
| } | |
| } | |
| alias KeepTerminator = Flag!"keepTerminator"; | |
| /+ | |
| struct ByLineBuffer(Char) | |
| { | |
| bool linePresent; | |
| bool EOF; | |
| Char[] buffer; | |
| ubyte[] decodeRemainder; | |
| bool append(const(ubyte)[] data) | |
| { | |
| byLineBuffer ~= data; | |
| } | |
| @property bool linePresent() | |
| { | |
| return byLinePresent; | |
| } | |
| Char[] get() | |
| { | |
| if (!linePresent) | |
| { | |
| // Decode ubyte[] into Char[] until a Terminator is found. | |
| // If not Terminator is found and EOF is false then raise an | |
| // exception. | |
| } | |
| return byLineBuffer; | |
| } | |
| } | |
| ++/ | |
| /** HTTP/FTP fetch content as a range of lines. | |
| * | |
| * A range of lines is returned when the request is complete. If the method or | |
| * other request properties is to be customized then set the `conn` parameter | |
| * with a HTTP/FTP instance that has these properties set. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * foreach (line; byLine("dlang.org")) | |
| * writeln(line); | |
| * ---- | |
| * | |
| * Params: | |
| * url = The url to receive content from | |
| * keepTerminator = `Yes.keepTerminator` signals that the line terminator should be | |
| * returned as part of the lines in the range. | |
| * terminator = The character that terminates a line | |
| * conn = The connection to use e.g. HTTP or FTP. | |
| * | |
| * Returns: | |
| * A range of Char[] with the content of the resource pointer to by the URL | |
| */ | |
| auto byLine(Conn = AutoProtocol, Terminator = char, Char = char) | |
| (const(char)[] url, KeepTerminator keepTerminator = No.keepTerminator, | |
| Terminator terminator = '\n', Conn conn = Conn()) | |
| if (isCurlConn!Conn && isSomeChar!Char && isSomeChar!Terminator) | |
| { | |
| static struct SyncLineInputRange | |
| { | |
| private Char[] lines; | |
| private Char[] current; | |
| private bool currentValid; | |
| private bool keepTerminator; | |
| private Terminator terminator; | |
| this(Char[] lines, bool kt, Terminator terminator) | |
| { | |
| this.lines = lines; | |
| this.keepTerminator = kt; | |
| this.terminator = terminator; | |
| currentValid = true; | |
| popFront(); | |
| } | |
| @property @safe bool empty() | |
| { | |
| return !currentValid; | |
| } | |
| @property @safe Char[] front() | |
| { | |
| import std.exception : enforce; | |
| enforce!CurlException(currentValid, "Cannot call front() on empty range"); | |
| return current; | |
| } | |
| void popFront() | |
| { | |
| import std.algorithm.searching : findSplitAfter, findSplit; | |
| import std.exception : enforce; | |
| enforce!CurlException(currentValid, "Cannot call popFront() on empty range"); | |
| if (lines.empty) | |
| { | |
| currentValid = false; | |
| return; | |
| } | |
| if (keepTerminator) | |
| { | |
| auto r = findSplitAfter(lines, [ terminator ]); | |
| if (r[0].empty) | |
| { | |
| current = r[1]; | |
| lines = r[0]; | |
| } | |
| else | |
| { | |
| current = r[0]; | |
| lines = r[1]; | |
| } | |
| } | |
| else | |
| { | |
| auto r = findSplit(lines, [ terminator ]); | |
| current = r[0]; | |
| lines = r[2]; | |
| } | |
| } | |
| } | |
| auto result = _getForRange!Char(url, conn); | |
| return SyncLineInputRange(result, keepTerminator == Yes.keepTerminator, terminator); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| s.send(httpOK("Line1\nLine2\nLine3")); | |
| }); | |
| assert(byLine(host).equal(["Line1", "Line2", "Line3"])); | |
| } | |
| } | |
| /** HTTP/FTP fetch content as a range of chunks. | |
| * | |
| * A range of chunks is returned when the request is complete. If the method or | |
| * other request properties is to be customized then set the `conn` parameter | |
| * with a HTTP/FTP instance that has these properties set. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * foreach (chunk; byChunk("dlang.org", 100)) | |
| * writeln(chunk); // chunk is ubyte[100] | |
| * ---- | |
| * | |
| * Params: | |
| * url = The url to receive content from | |
| * chunkSize = The size of each chunk | |
| * conn = The connection to use e.g. HTTP or FTP. | |
| * | |
| * Returns: | |
| * A range of ubyte[chunkSize] with the content of the resource pointer to by the URL | |
| */ | |
| auto byChunk(Conn = AutoProtocol) | |
| (const(char)[] url, size_t chunkSize = 1024, Conn conn = Conn()) | |
| if (isCurlConn!(Conn)) | |
| { | |
| static struct SyncChunkInputRange | |
| { | |
| private size_t chunkSize; | |
| private ubyte[] _bytes; | |
| private size_t offset; | |
| this(ubyte[] bytes, size_t chunkSize) | |
| { | |
| this._bytes = bytes; | |
| this.chunkSize = chunkSize; | |
| } | |
| @property @safe auto empty() | |
| { | |
| return offset == _bytes.length; | |
| } | |
| @property ubyte[] front() | |
| { | |
| size_t nextOffset = offset + chunkSize; | |
| if (nextOffset > _bytes.length) nextOffset = _bytes.length; | |
| return _bytes[offset .. nextOffset]; | |
| } | |
| @safe void popFront() | |
| { | |
| offset += chunkSize; | |
| if (offset > _bytes.length) offset = _bytes.length; | |
| } | |
| } | |
| auto result = _getForRange!ubyte(url, conn); | |
| return SyncChunkInputRange(result, chunkSize); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| s.send(httpOK(cast(ubyte[])[0, 1, 2, 3, 4, 5])); | |
| }); | |
| assert(byChunk(host, 2).equal([[0, 1], [2, 3], [4, 5]])); | |
| } | |
| } | |
| private T[] _getForRange(T,Conn)(const(char)[] url, Conn conn) | |
| { | |
| static if (is(Conn : HTTP)) | |
| { | |
| conn.method = conn.method == HTTP.Method.undefined ? HTTP.Method.get : conn.method; | |
| return _basicHTTP!(T)(url, null, conn); | |
| } | |
| else static if (is(Conn : FTP)) | |
| { | |
| return _basicFTP!(T)(url, null, conn); | |
| } | |
| else | |
| { | |
| if (isFTPUrl(url)) | |
| return get!(FTP,T)(url, FTP()); | |
| else | |
| return get!(HTTP,T)(url, HTTP()); | |
| } | |
| } | |
| /* | |
| Main thread part of the message passing protocol used for all async | |
| curl protocols. | |
| */ | |
| private mixin template WorkerThreadProtocol(Unit, alias units) | |
| { | |
| import core.time : Duration; | |
| @property bool empty() | |
| { | |
| tryEnsureUnits(); | |
| return state == State.done; | |
| } | |
| @property Unit[] front() | |
| { | |
| import std.format : format; | |
| tryEnsureUnits(); | |
| assert(state == State.gotUnits, | |
| format("Expected %s but got $s", | |
| State.gotUnits, state)); | |
| return units; | |
| } | |
| void popFront() | |
| { | |
| import std.concurrency : send; | |
| import std.format : format; | |
| tryEnsureUnits(); | |
| assert(state == State.gotUnits, | |
| format("Expected %s but got $s", | |
| State.gotUnits, state)); | |
| state = State.needUnits; | |
| // Send to worker thread for buffer reuse | |
| workerTid.send(cast(immutable(Unit)[]) units); | |
| units = null; | |
| } | |
| /** Wait for duration or until data is available and return true if data is | |
| available | |
| */ | |
| bool wait(Duration d) | |
| { | |
| import core.time : dur; | |
| import std.datetime.stopwatch : StopWatch; | |
| import std.concurrency : receiveTimeout; | |
| if (state == State.gotUnits) | |
| return true; | |
| enum noDur = dur!"hnsecs"(0); | |
| StopWatch sw; | |
| sw.start(); | |
| while (state != State.gotUnits && d > noDur) | |
| { | |
| final switch (state) | |
| { | |
| case State.needUnits: | |
| receiveTimeout(d, | |
| (Tid origin, CurlMessage!(immutable(Unit)[]) _data) | |
| { | |
| if (origin != workerTid) | |
| return false; | |
| units = cast(Unit[]) _data.data; | |
| state = State.gotUnits; | |
| return true; | |
| }, | |
| (Tid origin, CurlMessage!bool f) | |
| { | |
| if (origin != workerTid) | |
| return false; | |
| state = state.done; | |
| return true; | |
| } | |
| ); | |
| break; | |
| case State.gotUnits: return true; | |
| case State.done: | |
| return false; | |
| } | |
| d -= sw.peek(); | |
| sw.reset(); | |
| } | |
| return state == State.gotUnits; | |
| } | |
| enum State | |
| { | |
| needUnits, | |
| gotUnits, | |
| done | |
| } | |
| State state; | |
| void tryEnsureUnits() | |
| { | |
| import std.concurrency : receive; | |
| while (true) | |
| { | |
| final switch (state) | |
| { | |
| case State.needUnits: | |
| receive( | |
| (Tid origin, CurlMessage!(immutable(Unit)[]) _data) | |
| { | |
| if (origin != workerTid) | |
| return false; | |
| units = cast(Unit[]) _data.data; | |
| state = State.gotUnits; | |
| return true; | |
| }, | |
| (Tid origin, CurlMessage!bool f) | |
| { | |
| if (origin != workerTid) | |
| return false; | |
| state = state.done; | |
| return true; | |
| } | |
| ); | |
| break; | |
| case State.gotUnits: return; | |
| case State.done: | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| /** HTTP/FTP fetch content as a range of lines asynchronously. | |
| * | |
| * A range of lines is returned immediately and the request that fetches the | |
| * lines is performed in another thread. If the method or other request | |
| * properties is to be customized then set the `conn` parameter with a | |
| * HTTP/FTP instance that has these properties set. | |
| * | |
| * If `postData` is non-_null the method will be set to `post` for HTTP | |
| * requests. | |
| * | |
| * The background thread will buffer up to transmitBuffers number of lines | |
| * before it stops receiving data from network. When the main thread reads the | |
| * lines from the range it frees up buffers and allows for the background thread | |
| * to receive more data from the network. | |
| * | |
| * If no data is available and the main thread accesses the range it will block | |
| * until data becomes available. An exception to this is the `wait(Duration)` method on | |
| * the $(LREF LineInputRange). This method will wait at maximum for the | |
| * specified duration and return true if data is available. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * // Get some pages in the background | |
| * auto range1 = byLineAsync("www.google.com"); | |
| * auto range2 = byLineAsync("www.wikipedia.org"); | |
| * foreach (line; byLineAsync("dlang.org")) | |
| * writeln(line); | |
| * | |
| * // Lines already fetched in the background and ready | |
| * foreach (line; range1) writeln(line); | |
| * foreach (line; range2) writeln(line); | |
| * ---- | |
| * | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * // Get a line in a background thread and wait in | |
| * // main thread for 2 seconds for it to arrive. | |
| * auto range3 = byLineAsync("dlang.com"); | |
| * if (range3.wait(dur!"seconds"(2))) | |
| * writeln(range3.front); | |
| * else | |
| * writeln("No line received after 2 seconds!"); | |
| * ---- | |
| * | |
| * Params: | |
| * url = The url to receive content from | |
| * postData = Data to HTTP Post | |
| * keepTerminator = `Yes.keepTerminator` signals that the line terminator should be | |
| * returned as part of the lines in the range. | |
| * terminator = The character that terminates a line | |
| * transmitBuffers = The number of lines buffered asynchronously | |
| * conn = The connection to use e.g. HTTP or FTP. | |
| * | |
| * Returns: | |
| * A range of Char[] with the content of the resource pointer to by the | |
| * URL. | |
| */ | |
| auto byLineAsync(Conn = AutoProtocol, Terminator = char, Char = char, PostUnit) | |
| (const(char)[] url, const(PostUnit)[] postData, | |
| KeepTerminator keepTerminator = No.keepTerminator, | |
| Terminator terminator = '\n', | |
| size_t transmitBuffers = 10, Conn conn = Conn()) | |
| if (isCurlConn!Conn && isSomeChar!Char && isSomeChar!Terminator) | |
| { | |
| static if (is(Conn : AutoProtocol)) | |
| { | |
| if (isFTPUrl(url)) | |
| return byLineAsync(url, postData, keepTerminator, | |
| terminator, transmitBuffers, FTP()); | |
| else | |
| return byLineAsync(url, postData, keepTerminator, | |
| terminator, transmitBuffers, HTTP()); | |
| } | |
| else | |
| { | |
| import std.concurrency : OnCrowding, send, setMaxMailboxSize, spawn, thisTid, Tid; | |
| // 50 is just an arbitrary number for now | |
| setMaxMailboxSize(thisTid, 50, OnCrowding.block); | |
| auto tid = spawn(&_async!().spawn!(Conn, Char, Terminator)); | |
| tid.send(thisTid); | |
| tid.send(terminator); | |
| tid.send(keepTerminator == Yes.keepTerminator); | |
| _async!().duplicateConnection(url, conn, postData, tid); | |
| return _async!().LineInputRange!Char(tid, transmitBuffers, | |
| Conn.defaultAsyncStringBufferSize); | |
| } | |
| } | |
| /// ditto | |
| auto byLineAsync(Conn = AutoProtocol, Terminator = char, Char = char) | |
| (const(char)[] url, KeepTerminator keepTerminator = No.keepTerminator, | |
| Terminator terminator = '\n', | |
| size_t transmitBuffers = 10, Conn conn = Conn()) | |
| { | |
| static if (is(Conn : AutoProtocol)) | |
| { | |
| if (isFTPUrl(url)) | |
| return byLineAsync(url, cast(void[]) null, keepTerminator, | |
| terminator, transmitBuffers, FTP()); | |
| else | |
| return byLineAsync(url, cast(void[]) null, keepTerminator, | |
| terminator, transmitBuffers, HTTP()); | |
| } | |
| else | |
| { | |
| return byLineAsync(url, cast(void[]) null, keepTerminator, | |
| terminator, transmitBuffers, conn); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| s.send(httpOK("Line1\nLine2\nLine3")); | |
| }); | |
| assert(byLineAsync(host).equal(["Line1", "Line2", "Line3"])); | |
| } | |
| } | |
| /** HTTP/FTP fetch content as a range of chunks asynchronously. | |
| * | |
| * A range of chunks is returned immediately and the request that fetches the | |
| * chunks is performed in another thread. If the method or other request | |
| * properties is to be customized then set the `conn` parameter with a | |
| * HTTP/FTP instance that has these properties set. | |
| * | |
| * If `postData` is non-_null the method will be set to `post` for HTTP | |
| * requests. | |
| * | |
| * The background thread will buffer up to transmitBuffers number of chunks | |
| * before is stops receiving data from network. When the main thread reads the | |
| * chunks from the range it frees up buffers and allows for the background | |
| * thread to receive more data from the network. | |
| * | |
| * If no data is available and the main thread access the range it will block | |
| * until data becomes available. An exception to this is the `wait(Duration)` | |
| * method on the $(LREF ChunkInputRange). This method will wait at maximum for the specified | |
| * duration and return true if data is available. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * // Get some pages in the background | |
| * auto range1 = byChunkAsync("www.google.com", 100); | |
| * auto range2 = byChunkAsync("www.wikipedia.org"); | |
| * foreach (chunk; byChunkAsync("dlang.org")) | |
| * writeln(chunk); // chunk is ubyte[100] | |
| * | |
| * // Chunks already fetched in the background and ready | |
| * foreach (chunk; range1) writeln(chunk); | |
| * foreach (chunk; range2) writeln(chunk); | |
| * ---- | |
| * | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * // Get a line in a background thread and wait in | |
| * // main thread for 2 seconds for it to arrive. | |
| * auto range3 = byChunkAsync("dlang.com", 10); | |
| * if (range3.wait(dur!"seconds"(2))) | |
| * writeln(range3.front); | |
| * else | |
| * writeln("No chunk received after 2 seconds!"); | |
| * ---- | |
| * | |
| * Params: | |
| * url = The url to receive content from | |
| * postData = Data to HTTP Post | |
| * chunkSize = The size of the chunks | |
| * transmitBuffers = The number of chunks buffered asynchronously | |
| * conn = The connection to use e.g. HTTP or FTP. | |
| * | |
| * Returns: | |
| * A range of ubyte[chunkSize] with the content of the resource pointer to by | |
| * the URL. | |
| */ | |
| auto byChunkAsync(Conn = AutoProtocol, PostUnit) | |
| (const(char)[] url, const(PostUnit)[] postData, | |
| size_t chunkSize = 1024, size_t transmitBuffers = 10, | |
| Conn conn = Conn()) | |
| if (isCurlConn!(Conn)) | |
| { | |
| static if (is(Conn : AutoProtocol)) | |
| { | |
| if (isFTPUrl(url)) | |
| return byChunkAsync(url, postData, chunkSize, | |
| transmitBuffers, FTP()); | |
| else | |
| return byChunkAsync(url, postData, chunkSize, | |
| transmitBuffers, HTTP()); | |
| } | |
| else | |
| { | |
| import std.concurrency : OnCrowding, send, setMaxMailboxSize, spawn, thisTid, Tid; | |
| // 50 is just an arbitrary number for now | |
| setMaxMailboxSize(thisTid, 50, OnCrowding.block); | |
| auto tid = spawn(&_async!().spawn!(Conn, ubyte)); | |
| tid.send(thisTid); | |
| _async!().duplicateConnection(url, conn, postData, tid); | |
| return _async!().ChunkInputRange(tid, transmitBuffers, chunkSize); | |
| } | |
| } | |
| /// ditto | |
| auto byChunkAsync(Conn = AutoProtocol) | |
| (const(char)[] url, | |
| size_t chunkSize = 1024, size_t transmitBuffers = 10, | |
| Conn conn = Conn()) | |
| if (isCurlConn!(Conn)) | |
| { | |
| static if (is(Conn : AutoProtocol)) | |
| { | |
| if (isFTPUrl(url)) | |
| return byChunkAsync(url, cast(void[]) null, chunkSize, | |
| transmitBuffers, FTP()); | |
| else | |
| return byChunkAsync(url, cast(void[]) null, chunkSize, | |
| transmitBuffers, HTTP()); | |
| } | |
| else | |
| { | |
| return byChunkAsync(url, cast(void[]) null, chunkSize, | |
| transmitBuffers, conn); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.comparison : equal; | |
| foreach (host; [testServer.addr, "http://"~testServer.addr]) | |
| { | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| s.send(httpOK(cast(ubyte[])[0, 1, 2, 3, 4, 5])); | |
| }); | |
| assert(byChunkAsync(host, 2).equal([[0, 1], [2, 3], [4, 5]])); | |
| } | |
| } | |
| /* | |
| Mixin template for all supported curl protocols. This is the commom | |
| functionallity such as timeouts and network interface settings. This should | |
| really be in the HTTP/FTP/SMTP structs but the documentation tool does not | |
| support a mixin to put its doc strings where a mixin is done. Therefore docs | |
| in this template is copied into each of HTTP/FTP/SMTP below. | |
| */ | |
| private mixin template Protocol() | |
| { | |
| import etc.c.curl : CurlReadFunc, RawCurlProxy = CurlProxy; | |
| import core.time : Duration; | |
| import std.socket : InternetAddress; | |
| /// Value to return from `onSend`/`onReceive` delegates in order to | |
| /// pause a request | |
| alias requestPause = CurlReadFunc.pause; | |
| /// Value to return from onSend delegate in order to abort a request | |
| alias requestAbort = CurlReadFunc.abort; | |
| static uint defaultAsyncStringBufferSize = 100; | |
| /** | |
| The curl handle used by this connection. | |
| */ | |
| @property ref Curl handle() return | |
| { | |
| return p.curl; | |
| } | |
| /** | |
| True if the instance is stopped. A stopped instance is not usable. | |
| */ | |
| @property bool isStopped() | |
| { | |
| return p.curl.stopped; | |
| } | |
| /// Stop and invalidate this instance. | |
| void shutdown() | |
| { | |
| p.curl.shutdown(); | |
| } | |
| /** Set verbose. | |
| This will print request information to stderr. | |
| */ | |
| @property void verbose(bool on) | |
| { | |
| p.curl.set(CurlOption.verbose, on ? 1L : 0L); | |
| } | |
| // Connection settings | |
| /// Set timeout for activity on connection. | |
| @property void dataTimeout(Duration d) | |
| { | |
| p.curl.set(CurlOption.low_speed_limit, 1); | |
| p.curl.set(CurlOption.low_speed_time, d.total!"seconds"); | |
| } | |
| /** Set maximum time an operation is allowed to take. | |
| This includes dns resolution, connecting, data transfer, etc. | |
| */ | |
| @property void operationTimeout(Duration d) | |
| { | |
| p.curl.set(CurlOption.timeout_ms, d.total!"msecs"); | |
| } | |
| /// Set timeout for connecting. | |
| @property void connectTimeout(Duration d) | |
| { | |
| p.curl.set(CurlOption.connecttimeout_ms, d.total!"msecs"); | |
| } | |
| // Network settings | |
| /** Proxy | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy) | |
| */ | |
| @property void proxy(const(char)[] host) | |
| { | |
| p.curl.set(CurlOption.proxy, host); | |
| } | |
| /** Proxy port | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXYPORT, _proxy_port) | |
| */ | |
| @property void proxyPort(ushort port) | |
| { | |
| p.curl.set(CurlOption.proxyport, cast(long) port); | |
| } | |
| /// Type of proxy | |
| alias CurlProxy = RawCurlProxy; | |
| /** Proxy type | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy_type) | |
| */ | |
| @property void proxyType(CurlProxy type) | |
| { | |
| p.curl.set(CurlOption.proxytype, cast(long) type); | |
| } | |
| /// DNS lookup timeout. | |
| @property void dnsTimeout(Duration d) | |
| { | |
| p.curl.set(CurlOption.dns_cache_timeout, d.total!"msecs"); | |
| } | |
| /** | |
| * The network interface to use in form of the IP of the interface. | |
| * | |
| * Example: | |
| * ---- | |
| * theprotocol.netInterface = "192.168.1.32"; | |
| * theprotocol.netInterface = [ 192, 168, 1, 32 ]; | |
| * ---- | |
| * | |
| * See: $(REF InternetAddress, std,socket) | |
| */ | |
| @property void netInterface(const(char)[] i) | |
| { | |
| p.curl.set(CurlOption.intrface, i); | |
| } | |
| /// ditto | |
| @property void netInterface(const(ubyte)[4] i) | |
| { | |
| import std.format : format; | |
| const str = format("%d.%d.%d.%d", i[0], i[1], i[2], i[3]); | |
| netInterface = str; | |
| } | |
| /// ditto | |
| @property void netInterface(InternetAddress i) | |
| { | |
| netInterface = i.toAddrString(); | |
| } | |
| /** | |
| Set the local outgoing port to use. | |
| Params: | |
| port = the first outgoing port number to try and use | |
| */ | |
| @property void localPort(ushort port) | |
| { | |
| p.curl.set(CurlOption.localport, cast(long) port); | |
| } | |
| /** | |
| Set the no proxy flag for the specified host names. | |
| Params: | |
| test = a list of comma host names that do not require | |
| proxy to get reached | |
| */ | |
| void setNoProxy(string hosts) | |
| { | |
| p.curl.set(CurlOption.noproxy, hosts); | |
| } | |
| /** | |
| Set the local outgoing port range to use. | |
| This can be used together with the localPort property. | |
| Params: | |
| range = if the first port is occupied then try this many | |
| port number forwards | |
| */ | |
| @property void localPortRange(ushort range) | |
| { | |
| p.curl.set(CurlOption.localportrange, cast(long) range); | |
| } | |
| /** Set the tcp no-delay socket option on or off. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTTCPNODELAY, nodelay) | |
| */ | |
| @property void tcpNoDelay(bool on) | |
| { | |
| p.curl.set(CurlOption.tcp_nodelay, cast(long) (on ? 1 : 0) ); | |
| } | |
| /** Sets whether SSL peer certificates should be verified. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTSSLVERIFYPEER, verifypeer) | |
| */ | |
| @property void verifyPeer(bool on) | |
| { | |
| p.curl.set(CurlOption.ssl_verifypeer, on ? 1 : 0); | |
| } | |
| /** Sets whether the host within an SSL certificate should be verified. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTSSLVERIFYHOST, verifypeer) | |
| */ | |
| @property void verifyHost(bool on) | |
| { | |
| p.curl.set(CurlOption.ssl_verifyhost, on ? 2 : 0); | |
| } | |
| // Authentication settings | |
| /** | |
| Set the user name, password and optionally domain for authentication | |
| purposes. | |
| Some protocols may need authentication in some cases. Use this | |
| function to provide credentials. | |
| Params: | |
| username = the username | |
| password = the password | |
| domain = used for NTLM authentication only and is set to the NTLM domain | |
| name | |
| */ | |
| void setAuthentication(const(char)[] username, const(char)[] password, | |
| const(char)[] domain = "") | |
| { | |
| import std.format : format; | |
| if (!domain.empty) | |
| username = format("%s/%s", domain, username); | |
| p.curl.set(CurlOption.userpwd, format("%s:%s", username, password)); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq; | |
| assert(req.hdrs.canFind("GET /")); | |
| assert(req.hdrs.canFind("Basic dXNlcjpwYXNz")); | |
| s.send(httpOK()); | |
| }); | |
| auto http = HTTP(testServer.addr); | |
| http.onReceive = (ubyte[] data) { return data.length; }; | |
| http.setAuthentication("user", "pass"); | |
| http.perform(); | |
| // https://issues.dlang.org/show_bug.cgi?id=17540 | |
| http.setNoProxy("www.example.com"); | |
| } | |
| /** | |
| Set the user name and password for proxy authentication. | |
| Params: | |
| username = the username | |
| password = the password | |
| */ | |
| void setProxyAuthentication(const(char)[] username, const(char)[] password) | |
| { | |
| import std.array : replace; | |
| import std.format : format; | |
| p.curl.set(CurlOption.proxyuserpwd, | |
| format("%s:%s", | |
| username.replace(":", "%3A"), | |
| password.replace(":", "%3A")) | |
| ); | |
| } | |
| /** | |
| * The event handler that gets called when data is needed for sending. The | |
| * length of the `void[]` specifies the maximum number of bytes that can | |
| * be sent. | |
| * | |
| * Returns: | |
| * The callback returns the number of elements in the buffer that have been | |
| * filled and are ready to send. | |
| * The special value `.abortRequest` can be returned in order to abort the | |
| * current request. | |
| * The special value `.pauseRequest` can be returned in order to pause the | |
| * current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * string msg = "Hello world"; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onSend = delegate size_t(void[] data) | |
| * { | |
| * auto m = cast(void[]) msg; | |
| * size_t length = m.length > data.length ? data.length : m.length; | |
| * if (length == 0) return 0; | |
| * data[0 .. length] = m[0 .. length]; | |
| * msg = msg[length..$]; | |
| * return length; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onSend(size_t delegate(void[]) callback) | |
| { | |
| p.curl.clear(CurlOption.postfields); // cannot specify data when using callback | |
| p.curl.onSend = callback; | |
| } | |
| /** | |
| * The event handler that receives incoming data. Be sure to copy the | |
| * incoming ubyte[] since it is not guaranteed to be valid after the | |
| * callback returns. | |
| * | |
| * Returns: | |
| * The callback returns the number of incoming bytes read. If the entire array is | |
| * not read the request will abort. | |
| * The special value .pauseRequest can be returned in order to pause the | |
| * current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onReceive = (ubyte[] data) | |
| * { | |
| * writeln("Got data", to!(const(char)[])(data)); | |
| * return data.length; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onReceive(size_t delegate(ubyte[]) callback) | |
| { | |
| p.curl.onReceive = callback; | |
| } | |
| /** | |
| * The event handler that gets called to inform of upload/download progress. | |
| * | |
| * Params: | |
| * dlTotal = total bytes to download | |
| * dlNow = currently downloaded bytes | |
| * ulTotal = total bytes to upload | |
| * ulNow = currently uploaded bytes | |
| * | |
| * Returns: | |
| * Return 0 from the callback to signal success, return non-zero to abort | |
| * transfer | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onProgress = delegate int(size_t dl, size_t dln, size_t ul, size_t uln) | |
| * { | |
| * writeln("Progress: downloaded ", dln, " of ", dl); | |
| * writeln("Progress: uploaded ", uln, " of ", ul); | |
| * return 0; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onProgress(int delegate(size_t dlTotal, size_t dlNow, | |
| size_t ulTotal, size_t ulNow) callback) | |
| { | |
| p.curl.onProgress = callback; | |
| } | |
| } | |
| /* | |
| Decode `ubyte[]` array using the provided EncodingScheme up to maxChars | |
| Returns: Tuple of ubytes read and the `Char[]` characters decoded. | |
| Not all ubytes are guaranteed to be read in case of decoding error. | |
| */ | |
| private Tuple!(size_t,Char[]) | |
| decodeString(Char = char)(const(ubyte)[] data, | |
| EncodingScheme scheme, | |
| size_t maxChars = size_t.max) | |
| { | |
| import std.encoding : INVALID_SEQUENCE; | |
| Char[] res; | |
| immutable startLen = data.length; | |
| size_t charsDecoded = 0; | |
| while (data.length && charsDecoded < maxChars) | |
| { | |
| immutable dchar dc = scheme.safeDecode(data); | |
| if (dc == INVALID_SEQUENCE) | |
| { | |
| return typeof(return)(size_t.max, cast(Char[]) null); | |
| } | |
| charsDecoded++; | |
| res ~= dc; | |
| } | |
| return typeof(return)(startLen-data.length, res); | |
| } | |
| /* | |
| Decode `ubyte[]` array using the provided `EncodingScheme` until a the | |
| line terminator specified is found. The basesrc parameter is effectively | |
| prepended to src as the first thing. | |
| This function is used for decoding as much of the src buffer as | |
| possible until either the terminator is found or decoding fails. If | |
| it fails as the last data in the src it may mean that the src buffer | |
| were missing some bytes in order to represent a correct code | |
| point. Upon the next call to this function more bytes have been | |
| received from net and the failing bytes should be given as the | |
| basesrc parameter. It is done this way to minimize data copying. | |
| Returns: true if a terminator was found | |
| Not all ubytes are guaranteed to be read in case of decoding error. | |
| any decoded chars will be inserted into dst. | |
| */ | |
| private bool decodeLineInto(Terminator, Char = char)(ref const(ubyte)[] basesrc, | |
| ref const(ubyte)[] src, | |
| ref Char[] dst, | |
| EncodingScheme scheme, | |
| Terminator terminator) | |
| { | |
| import std.algorithm.searching : endsWith; | |
| import std.encoding : INVALID_SEQUENCE; | |
| import std.exception : enforce; | |
| // if there is anything in the basesrc then try to decode that | |
| // first. | |
| if (basesrc.length != 0) | |
| { | |
| // Try to ensure 4 entries in the basesrc by copying from src. | |
| immutable blen = basesrc.length; | |
| immutable len = (basesrc.length + src.length) >= 4 ? | |
| 4 : basesrc.length + src.length; | |
| basesrc.length = len; | |
| immutable dchar dc = scheme.safeDecode(basesrc); | |
| if (dc == INVALID_SEQUENCE) | |
| { | |
| enforce!CurlException(len != 4, "Invalid code sequence"); | |
| return false; | |
| } | |
| dst ~= dc; | |
| src = src[len-basesrc.length-blen .. $]; // remove used ubytes from src | |
| basesrc.length = 0; | |
| } | |
| while (src.length) | |
| { | |
| const lsrc = src; | |
| dchar dc = scheme.safeDecode(src); | |
| if (dc == INVALID_SEQUENCE) | |
| { | |
| if (src.empty) | |
| { | |
| // The invalid sequence was in the end of the src. Maybe there | |
| // just need to be more bytes available so these last bytes are | |
| // put back to src for later use. | |
| src = lsrc; | |
| return false; | |
| } | |
| dc = '?'; | |
| } | |
| dst ~= dc; | |
| if (dst.endsWith(terminator)) | |
| return true; | |
| } | |
| return false; // no terminator found | |
| } | |
| /** | |
| * HTTP client functionality. | |
| * | |
| * Example: | |
| * | |
| * Get with custom data receivers: | |
| * | |
| * --- | |
| * import std.net.curl, std.stdio; | |
| * | |
| * auto http = HTTP("https://dlang.org"); | |
| * http.onReceiveHeader = | |
| * (in char[] key, in char[] value) { writeln(key ~ ": " ~ value); }; | |
| * http.onReceive = (ubyte[] data) { /+ drop +/ return data.length; }; | |
| * http.perform(); | |
| * --- | |
| * | |
| */ | |
| /** | |
| * Put with data senders: | |
| * | |
| * --- | |
| * import std.net.curl, std.stdio; | |
| * | |
| * auto http = HTTP("https://dlang.org"); | |
| * auto msg = "Hello world"; | |
| * http.contentLength = msg.length; | |
| * http.onSend = (void[] data) | |
| * { | |
| * auto m = cast(void[]) msg; | |
| * size_t len = m.length > data.length ? data.length : m.length; | |
| * if (len == 0) return len; | |
| * data[0 .. len] = m[0 .. len]; | |
| * msg = msg[len..$]; | |
| * return len; | |
| * }; | |
| * http.perform(); | |
| * --- | |
| * | |
| */ | |
| /** | |
| * Tracking progress: | |
| * | |
| * --- | |
| * import std.net.curl, std.stdio; | |
| * | |
| * auto http = HTTP(); | |
| * http.method = HTTP.Method.get; | |
| * http.url = "http://upload.wikimedia.org/wikipedia/commons/" ~ | |
| * "5/53/Wikipedia-logo-en-big.png"; | |
| * http.onReceive = (ubyte[] data) { return data.length; }; | |
| * http.onProgress = (size_t dltotal, size_t dlnow, | |
| * size_t ultotal, size_t ulnow) | |
| * { | |
| * writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow); | |
| * return 0; | |
| * }; | |
| * http.perform(); | |
| * --- | |
| * | |
| * See_Also: $(LINK2 http://www.ietf.org/rfc/rfc2616.txt, RFC2616) | |
| * | |
| */ | |
| struct HTTP | |
| { | |
| mixin Protocol; | |
| import std.datetime.systime : SysTime; | |
| import std.typecons : RefCounted; | |
| import etc.c.curl : CurlAuth, CurlInfo, curl_slist, CURLVERSION_NOW, curl_off_t; | |
| /// Authentication method equal to $(REF CurlAuth, etc,c,curl) | |
| alias AuthMethod = CurlAuth; | |
| static private uint defaultMaxRedirects = 10; | |
| private struct Impl | |
| { | |
| ~this() | |
| { | |
| if (headersOut !is null) | |
| Curl.curl.slist_free_all(headersOut); | |
| if (curl.handle !is null) // work around RefCounted/emplace bug | |
| curl.shutdown(); | |
| } | |
| Curl curl; | |
| curl_slist* headersOut; | |
| string[string] headersIn; | |
| string charset; | |
| /// The status line of the final sub-request in a request. | |
| StatusLine status; | |
| private void delegate(StatusLine) onReceiveStatusLine; | |
| /// The HTTP method to use. | |
| Method method = Method.undefined; | |
| @system @property void onReceiveHeader(void delegate(in char[] key, | |
| in char[] value) callback) | |
| { | |
| import std.algorithm.searching : findSplit, startsWith; | |
| import std.string : indexOf, chomp; | |
| import std.uni : toLower; | |
| import std.exception : assumeUnique; | |
| // Wrap incoming callback in order to separate http status line from | |
| // http headers. On redirected requests there may be several such | |
| // status lines. The last one is the one recorded. | |
| auto dg = (in char[] header) | |
| { | |
| import std.utf : UTFException; | |
| try | |
| { | |
| if (header.empty) | |
| { | |
| // header delimiter | |
| return; | |
| } | |
| if (header.startsWith("HTTP/")) | |
| { | |
| headersIn.clear(); | |
| if (parseStatusLine(header, status)) | |
| { | |
| if (onReceiveStatusLine != null) | |
| onReceiveStatusLine(status); | |
| } | |
| return; | |
| } | |
| auto m = header.findSplit(": "); | |
| const(char)[] lowerFieldName = m[0].toLower(); | |
| ///Fixes https://issues.dlang.org/show_bug.cgi?id=24458 | |
| string fieldName = lowerFieldName is m[0] ? lowerFieldName.idup : assumeUnique(lowerFieldName); | |
| auto fieldContent = m[2].chomp; | |
| if (fieldName == "content-type") | |
| { | |
| auto io = indexOf(fieldContent, "charset=", No.caseSensitive); | |
| if (io != -1) | |
| charset = fieldContent[io + "charset=".length .. $].findSplit(";")[0].idup; | |
| } | |
| if (!m[1].empty && callback !is null) | |
| callback(fieldName, fieldContent); | |
| headersIn[fieldName] = fieldContent.idup; | |
| } | |
| catch (UTFException e) | |
| { | |
| //munch it - a header should be all ASCII, any "wrong UTF" is broken header | |
| } | |
| }; | |
| curl.onReceiveHeader = dg; | |
| } | |
| } | |
| private RefCounted!Impl p; | |
| import etc.c.curl : CurlTimeCond; | |
| /// Parse status line, as received from / generated by cURL. | |
| private static bool parseStatusLine(const char[] header, out StatusLine status) @safe | |
| { | |
| import std.algorithm.searching : findSplit, startsWith; | |
| import std.conv : to, ConvException; | |
| if (!header.startsWith("HTTP/")) | |
| return false; | |
| try | |
| { | |
| const m = header["HTTP/".length .. $].findSplit(" "); | |
| const v = m[0].findSplit("."); | |
| status.majorVersion = to!ushort(v[0]); | |
| status.minorVersion = v[1].length ? to!ushort(v[2]) : 0; | |
| const s2 = m[2].findSplit(" "); | |
| status.code = to!ushort(s2[0]); | |
| status.reason = s2[2].idup; | |
| return true; | |
| } | |
| catch (ConvException e) | |
| { | |
| return false; | |
| } | |
| } | |
| @safe unittest | |
| { | |
| StatusLine status; | |
| assert(parseStatusLine("HTTP/1.1 200 OK", status) | |
| && status == StatusLine(1, 1, 200, "OK")); | |
| assert(parseStatusLine("HTTP/1.0 304 Not Modified", status) | |
| && status == StatusLine(1, 0, 304, "Not Modified")); | |
| // The HTTP2 protocol is binary; cURL generates this fake text header. | |
| assert(parseStatusLine("HTTP/2 200", status) | |
| && status == StatusLine(2, 0, 200, null)); | |
| assert(!parseStatusLine("HTTP/2", status)); | |
| assert(!parseStatusLine("HTTP/2 -1", status)); | |
| assert(!parseStatusLine("HTTP/2 200", status)); | |
| assert(!parseStatusLine("HTTP/2.X 200", status)); | |
| assert(!parseStatusLine("HTTP|2 200", status)); | |
| } | |
| /** Time condition enumeration as an alias of $(REF CurlTimeCond, etc,c,curl) | |
| $(HTTP www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25, _RFC2616 Section 14.25) | |
| */ | |
| alias TimeCond = CurlTimeCond; | |
| /** | |
| Constructor taking the url as parameter. | |
| */ | |
| static HTTP opCall(const(char)[] url) | |
| { | |
| HTTP http; | |
| http.initialize(); | |
| http.url = url; | |
| return http; | |
| } | |
| /// | |
| static HTTP opCall() | |
| { | |
| HTTP http; | |
| http.initialize(); | |
| return http; | |
| } | |
| /// | |
| HTTP dup() | |
| { | |
| HTTP copy; | |
| copy.initialize(); | |
| copy.p.method = p.method; | |
| curl_slist* cur = p.headersOut; | |
| curl_slist* newlist = null; | |
| while (cur) | |
| { | |
| newlist = Curl.curl.slist_append(newlist, cur.data); | |
| cur = cur.next; | |
| } | |
| copy.p.headersOut = newlist; | |
| copy.p.curl.set(CurlOption.httpheader, copy.p.headersOut); | |
| copy.p.curl = p.curl.dup(); | |
| copy.dataTimeout = _defaultDataTimeout; | |
| copy.onReceiveHeader = null; | |
| return copy; | |
| } | |
| private void initialize() | |
| { | |
| p.curl.initialize(); | |
| maxRedirects = HTTP.defaultMaxRedirects; | |
| p.charset = "ISO-8859-1"; // Default charset defined in HTTP RFC | |
| p.method = Method.undefined; | |
| setUserAgent(HTTP.defaultUserAgent); | |
| dataTimeout = _defaultDataTimeout; | |
| onReceiveHeader = null; | |
| verifyPeer = true; | |
| verifyHost = true; | |
| } | |
| /** | |
| Perform a http request. | |
| After the HTTP client has been setup and possibly assigned callbacks the | |
| `perform()` method will start performing the request towards the | |
| specified server. | |
| Params: | |
| throwOnError = whether to throw an exception or return a CurlCode on error | |
| */ | |
| CurlCode perform(ThrowOnError throwOnError = Yes.throwOnError) | |
| { | |
| p.status.reset(); | |
| CurlOption opt; | |
| final switch (p.method) | |
| { | |
| case Method.head: | |
| p.curl.set(CurlOption.nobody, 1L); | |
| opt = CurlOption.nobody; | |
| break; | |
| case Method.undefined: | |
| case Method.get: | |
| p.curl.set(CurlOption.httpget, 1L); | |
| opt = CurlOption.httpget; | |
| break; | |
| case Method.post: | |
| p.curl.set(CurlOption.post, 1L); | |
| opt = CurlOption.post; | |
| break; | |
| case Method.put: | |
| p.curl.set(CurlOption.upload, 1L); | |
| opt = CurlOption.upload; | |
| break; | |
| case Method.del: | |
| p.curl.set(CurlOption.customrequest, "DELETE"); | |
| opt = CurlOption.customrequest; | |
| break; | |
| case Method.options: | |
| p.curl.set(CurlOption.customrequest, "OPTIONS"); | |
| opt = CurlOption.customrequest; | |
| break; | |
| case Method.trace: | |
| p.curl.set(CurlOption.customrequest, "TRACE"); | |
| opt = CurlOption.customrequest; | |
| break; | |
| case Method.connect: | |
| p.curl.set(CurlOption.customrequest, "CONNECT"); | |
| opt = CurlOption.customrequest; | |
| break; | |
| case Method.patch: | |
| p.curl.set(CurlOption.customrequest, "PATCH"); | |
| opt = CurlOption.customrequest; | |
| break; | |
| } | |
| scope (exit) p.curl.clear(opt); | |
| return p.curl.perform(throwOnError); | |
| } | |
| /// The URL to specify the location of the resource. | |
| @property void url(const(char)[] url) | |
| { | |
| import std.algorithm.searching : startsWith; | |
| import std.uni : toLower; | |
| if (!startsWith(url.toLower(), "http://", "https://")) | |
| url = "http://" ~ url; | |
| p.curl.set(CurlOption.url, url); | |
| } | |
| /// Set the CA certificate bundle file to use for SSL peer verification | |
| @property void caInfo(const(char)[] caFile) | |
| { | |
| p.curl.set(CurlOption.cainfo, caFile); | |
| } | |
| // This is a workaround for mixed in content not having its | |
| // docs mixed in. | |
| version (StdDdoc) | |
| { | |
| static import etc.c.curl; | |
| /// Value to return from `onSend`/`onReceive` delegates in order to | |
| /// pause a request | |
| alias requestPause = CurlReadFunc.pause; | |
| /// Value to return from onSend delegate in order to abort a request | |
| alias requestAbort = CurlReadFunc.abort; | |
| /** | |
| True if the instance is stopped. A stopped instance is not usable. | |
| */ | |
| @property bool isStopped(); | |
| /// Stop and invalidate this instance. | |
| void shutdown(); | |
| /** Set verbose. | |
| This will print request information to stderr. | |
| */ | |
| @property void verbose(bool on); | |
| // Connection settings | |
| /// Set timeout for activity on connection. | |
| @property void dataTimeout(Duration d); | |
| /** Set maximum time an operation is allowed to take. | |
| This includes dns resolution, connecting, data transfer, etc. | |
| */ | |
| @property void operationTimeout(Duration d); | |
| /// Set timeout for connecting. | |
| @property void connectTimeout(Duration d); | |
| // Network settings | |
| /** Proxy | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy) | |
| */ | |
| @property void proxy(const(char)[] host); | |
| /** Proxy port | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXYPORT, _proxy_port) | |
| */ | |
| @property void proxyPort(ushort port); | |
| /// Type of proxy | |
| alias CurlProxy = etc.c.curl.CurlProxy; | |
| /** Proxy type | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy_type) | |
| */ | |
| @property void proxyType(CurlProxy type); | |
| /// DNS lookup timeout. | |
| @property void dnsTimeout(Duration d); | |
| /** | |
| * The network interface to use in form of the IP of the interface. | |
| * | |
| * Example: | |
| * ---- | |
| * theprotocol.netInterface = "192.168.1.32"; | |
| * theprotocol.netInterface = [ 192, 168, 1, 32 ]; | |
| * ---- | |
| * | |
| * See: $(REF InternetAddress, std,socket) | |
| */ | |
| @property void netInterface(const(char)[] i); | |
| /// ditto | |
| @property void netInterface(const(ubyte)[4] i); | |
| /// ditto | |
| @property void netInterface(InternetAddress i); | |
| /** | |
| Set the local outgoing port to use. | |
| Params: | |
| port = the first outgoing port number to try and use | |
| */ | |
| @property void localPort(ushort port); | |
| /** | |
| Set the local outgoing port range to use. | |
| This can be used together with the localPort property. | |
| Params: | |
| range = if the first port is occupied then try this many | |
| port number forwards | |
| */ | |
| @property void localPortRange(ushort range); | |
| /** Set the tcp no-delay socket option on or off. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTTCPNODELAY, nodelay) | |
| */ | |
| @property void tcpNoDelay(bool on); | |
| // Authentication settings | |
| /** | |
| Set the user name, password and optionally domain for authentication | |
| purposes. | |
| Some protocols may need authentication in some cases. Use this | |
| function to provide credentials. | |
| Params: | |
| username = the username | |
| password = the password | |
| domain = used for NTLM authentication only and is set to the NTLM domain | |
| name | |
| */ | |
| void setAuthentication(const(char)[] username, const(char)[] password, | |
| const(char)[] domain = ""); | |
| /** | |
| Set the user name and password for proxy authentication. | |
| Params: | |
| username = the username | |
| password = the password | |
| */ | |
| void setProxyAuthentication(const(char)[] username, const(char)[] password); | |
| /** | |
| * The event handler that gets called when data is needed for sending. The | |
| * length of the `void[]` specifies the maximum number of bytes that can | |
| * be sent. | |
| * | |
| * Returns: | |
| * The callback returns the number of elements in the buffer that have been | |
| * filled and are ready to send. | |
| * The special value `.abortRequest` can be returned in order to abort the | |
| * current request. | |
| * The special value `.pauseRequest` can be returned in order to pause the | |
| * current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * string msg = "Hello world"; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onSend = delegate size_t(void[] data) | |
| * { | |
| * auto m = cast(void[]) msg; | |
| * size_t length = m.length > data.length ? data.length : m.length; | |
| * if (length == 0) return 0; | |
| * data[0 .. length] = m[0 .. length]; | |
| * msg = msg[length..$]; | |
| * return length; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onSend(size_t delegate(void[]) callback); | |
| /** | |
| * The event handler that receives incoming data. Be sure to copy the | |
| * incoming ubyte[] since it is not guaranteed to be valid after the | |
| * callback returns. | |
| * | |
| * Returns: | |
| * The callback returns the incoming bytes read. If not the entire array is | |
| * the request will abort. | |
| * The special value .pauseRequest can be returned in order to pause the | |
| * current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onReceive = (ubyte[] data) | |
| * { | |
| * writeln("Got data", to!(const(char)[])(data)); | |
| * return data.length; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onReceive(size_t delegate(ubyte[]) callback); | |
| /** | |
| * Register an event handler that gets called to inform of | |
| * upload/download progress. | |
| * | |
| * Callback_parameters: | |
| * $(CALLBACK_PARAMS) | |
| * | |
| * Callback_returns: Return 0 to signal success, return non-zero to | |
| * abort transfer. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * auto client = HTTP("dlang.org"); | |
| * client.onProgress = delegate int(size_t dl, size_t dln, size_t ul, size_t uln) | |
| * { | |
| * writeln("Progress: downloaded ", dln, " of ", dl); | |
| * writeln("Progress: uploaded ", uln, " of ", ul); | |
| * return 0; | |
| * }; | |
| * client.perform(); | |
| * ---- | |
| */ | |
| @property void onProgress(int delegate(size_t dlTotal, size_t dlNow, | |
| size_t ulTotal, size_t ulNow) callback); | |
| } | |
| /** Clear all outgoing headers. | |
| */ | |
| void clearRequestHeaders() | |
| { | |
| if (p.headersOut !is null) | |
| Curl.curl.slist_free_all(p.headersOut); | |
| p.headersOut = null; | |
| p.curl.clear(CurlOption.httpheader); | |
| } | |
| /** Add a header e.g. "X-CustomField: Something is fishy". | |
| * | |
| * There is no remove header functionality. Do a $(LREF clearRequestHeaders) | |
| * and set the needed headers instead. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * auto client = HTTP(); | |
| * client.addRequestHeader("X-Custom-ABC", "This is the custom value"); | |
| * auto content = get("dlang.org", client); | |
| * --- | |
| */ | |
| void addRequestHeader(const(char)[] name, const(char)[] value) | |
| { | |
| import std.format : format; | |
| import std.internal.cstring : tempCString; | |
| import std.uni : icmp; | |
| if (icmp(name, "User-Agent") == 0) | |
| return setUserAgent(value); | |
| string nv = format("%s: %s", name, value); | |
| p.headersOut = Curl.curl.slist_append(p.headersOut, | |
| nv.tempCString().buffPtr); | |
| p.curl.set(CurlOption.httpheader, p.headersOut); | |
| } | |
| /** | |
| * The default "User-Agent" value send with a request. | |
| * It has the form "Phobos-std.net.curl/$(I PHOBOS_VERSION) (libcurl/$(I CURL_VERSION))" | |
| */ | |
| static string defaultUserAgent() @property | |
| { | |
| import std.compiler : version_major, version_minor; | |
| import std.format : format, sformat; | |
| // http://curl.haxx.se/docs/versions.html | |
| enum fmt = "Phobos-std.net.curl/%d.%03d (libcurl/%d.%d.%d)"; | |
| enum maxLen = fmt.length - "%d%03d%d%d%d".length + 10 + 10 + 3 + 3 + 3; | |
| static char[maxLen] buf = void; | |
| static string userAgent; | |
| if (!userAgent.length) | |
| { | |
| auto curlVer = Curl.curl.version_info(CURLVERSION_NOW).version_num; | |
| userAgent = cast(immutable) sformat( | |
| buf, fmt, version_major, version_minor, | |
| curlVer >> 16 & 0xFF, curlVer >> 8 & 0xFF, curlVer & 0xFF); | |
| } | |
| return userAgent; | |
| } | |
| /** Set the value of the user agent request header field. | |
| * | |
| * By default a request has it's "User-Agent" field set to $(LREF | |
| * defaultUserAgent) even if `setUserAgent` was never called. Pass | |
| * an empty string to suppress the "User-Agent" field altogether. | |
| */ | |
| void setUserAgent(const(char)[] userAgent) | |
| { | |
| p.curl.set(CurlOption.useragent, userAgent); | |
| } | |
| /** | |
| * Get various timings defined in $(REF CurlInfo, etc, c, curl). | |
| * The value is usable only if the return value is equal to `etc.c.curl.CurlError.ok`. | |
| * | |
| * Params: | |
| * timing = one of the timings defined in $(REF CurlInfo, etc, c, curl). | |
| * The values are: | |
| * `etc.c.curl.CurlInfo.namelookup_time`, | |
| * `etc.c.curl.CurlInfo.connect_time`, | |
| * `etc.c.curl.CurlInfo.pretransfer_time`, | |
| * `etc.c.curl.CurlInfo.starttransfer_time`, | |
| * `etc.c.curl.CurlInfo.redirect_time`, | |
| * `etc.c.curl.CurlInfo.appconnect_time`, | |
| * `etc.c.curl.CurlInfo.total_time`. | |
| * val = the actual value of the inquired timing. | |
| * | |
| * Returns: | |
| * The return code of the operation. The value stored in val | |
| * should be used only if the return value is `etc.c.curl.CurlInfo.ok`. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * import etc.c.curl : CurlError, CurlInfo; | |
| * | |
| * auto client = HTTP("dlang.org"); | |
| * client.perform(); | |
| * | |
| * double val; | |
| * CurlCode code; | |
| * | |
| * code = client.getTiming(CurlInfo.namelookup_time, val); | |
| * assert(code == CurlError.ok); | |
| * --- | |
| */ | |
| CurlCode getTiming(CurlInfo timing, ref double val) | |
| { | |
| return p.curl.getTiming(timing, val); | |
| } | |
| /** The headers read from a successful response. | |
| * | |
| */ | |
| @property string[string] responseHeaders() | |
| { | |
| return p.headersIn; | |
| } | |
| /// HTTP method used. | |
| @property void method(Method m) | |
| { | |
| p.method = m; | |
| } | |
| /// ditto | |
| @property Method method() | |
| { | |
| return p.method; | |
| } | |
| /** | |
| HTTP status line of last response. One call to perform may | |
| result in several requests because of redirection. | |
| */ | |
| @property StatusLine statusLine() | |
| { | |
| return p.status; | |
| } | |
| /// Set the active cookie string e.g. "name1=value1;name2=value2" | |
| void setCookie(const(char)[] cookie) | |
| { | |
| p.curl.set(CurlOption.cookie, cookie); | |
| } | |
| /// Set a file path to where a cookie jar should be read/stored. | |
| void setCookieJar(const(char)[] path) | |
| { | |
| p.curl.set(CurlOption.cookiefile, path); | |
| if (path.length) | |
| p.curl.set(CurlOption.cookiejar, path); | |
| } | |
| /// Flush cookie jar to disk. | |
| void flushCookieJar() | |
| { | |
| p.curl.set(CurlOption.cookielist, "FLUSH"); | |
| } | |
| /// Clear session cookies. | |
| void clearSessionCookies() | |
| { | |
| p.curl.set(CurlOption.cookielist, "SESS"); | |
| } | |
| /// Clear all cookies. | |
| void clearAllCookies() | |
| { | |
| p.curl.set(CurlOption.cookielist, "ALL"); | |
| } | |
| /** | |
| Set time condition on the request. | |
| Params: | |
| cond = `CurlTimeCond.{none,ifmodsince,ifunmodsince,lastmod}` | |
| timestamp = Timestamp for the condition | |
| $(HTTP www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25, _RFC2616 Section 14.25) | |
| */ | |
| void setTimeCondition(HTTP.TimeCond cond, SysTime timestamp) | |
| { | |
| p.curl.set(CurlOption.timecondition, cond); | |
| p.curl.set(CurlOption.timevalue, timestamp.toUnixTime()); | |
| } | |
| /** Specifying data to post when not using the onSend callback. | |
| * | |
| * The data is NOT copied by the library. Content-Type will default to | |
| * application/octet-stream. Data is not converted or encoded by this | |
| * method. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * auto http = HTTP("http://www.mydomain.com"); | |
| * http.onReceive = (ubyte[] data) { writeln(to!(const(char)[])(data)); return data.length; }; | |
| * http.postData = [1,2,3,4,5]; | |
| * http.perform(); | |
| * ---- | |
| */ | |
| @property void postData(const(void)[] data) | |
| { | |
| setPostData(data, "application/octet-stream"); | |
| } | |
| /** Specifying data to post when not using the onSend callback. | |
| * | |
| * The data is NOT copied by the library. Content-Type will default to | |
| * text/plain. Data is not converted or encoded by this method. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * auto http = HTTP("http://www.mydomain.com"); | |
| * http.onReceive = (ubyte[] data) { writeln(to!(const(char)[])(data)); return data.length; }; | |
| * http.postData = "The quick...."; | |
| * http.perform(); | |
| * ---- | |
| */ | |
| @property void postData(const(char)[] data) | |
| { | |
| setPostData(data, "text/plain"); | |
| } | |
| /** | |
| * Specify data to post when not using the onSend callback, with | |
| * user-specified Content-Type. | |
| * Params: | |
| * data = Data to post. | |
| * contentType = MIME type of the data, for example, "text/plain" or | |
| * "application/octet-stream". See also: | |
| * $(LINK2 http://en.wikipedia.org/wiki/Internet_media_type, | |
| * Internet media type) on Wikipedia. | |
| * ----- | |
| * import std.net.curl; | |
| * auto http = HTTP("http://onlineform.example.com"); | |
| * auto data = "app=login&username=bob&password=s00perS3kret"; | |
| * http.setPostData(data, "application/x-www-form-urlencoded"); | |
| * http.onReceive = (ubyte[] data) { return data.length; }; | |
| * http.perform(); | |
| * ----- | |
| */ | |
| void setPostData(const(void)[] data, string contentType) | |
| { | |
| // cannot use callback when specifying data directly so it is disabled here. | |
| p.curl.clear(CurlOption.readfunction); | |
| addRequestHeader("Content-Type", contentType); | |
| p.curl.set(CurlOption.postfields, cast(void*) data.ptr); | |
| p.curl.set(CurlOption.postfieldsize, data.length); | |
| if (method == Method.undefined) | |
| method = Method.post; | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.searching : canFind; | |
| testServer.handle((s) { | |
| auto req = s.recvReq!ubyte; | |
| assert(req.hdrs.canFind("POST /path")); | |
| assert(req.bdy.canFind(cast(ubyte[])[0, 1, 2, 3, 4])); | |
| assert(req.bdy.canFind(cast(ubyte[])[253, 254, 255])); | |
| s.send(httpOK(cast(ubyte[])[17, 27, 35, 41])); | |
| }); | |
| auto data = new ubyte[](256); | |
| foreach (i, ref ub; data) | |
| ub = cast(ubyte) i; | |
| auto http = HTTP(testServer.addr~"/path"); | |
| http.postData = data; | |
| ubyte[] res; | |
| http.onReceive = (data) { res ~= data; return data.length; }; | |
| http.perform(); | |
| assert(res == cast(ubyte[])[17, 27, 35, 41]); | |
| } | |
| /** | |
| * Set the event handler that receives incoming headers. | |
| * | |
| * The callback will receive a header field key, value as parameter. The | |
| * `const(char)[]` arrays are not valid after the delegate has returned. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * auto http = HTTP("dlang.org"); | |
| * http.onReceive = (ubyte[] data) { writeln(to!(const(char)[])(data)); return data.length; }; | |
| * http.onReceiveHeader = (in char[] key, in char[] value) { writeln(key, " = ", value); }; | |
| * http.perform(); | |
| * ---- | |
| */ | |
| @property void onReceiveHeader(void delegate(in char[] key, | |
| in char[] value) callback) | |
| { | |
| p.onReceiveHeader = callback; | |
| } | |
| /** | |
| Callback for each received StatusLine. | |
| Notice that several callbacks can be done for each call to | |
| `perform()` due to redirections. | |
| See_Also: $(LREF StatusLine) | |
| */ | |
| @property void onReceiveStatusLine(void delegate(StatusLine) callback) | |
| { | |
| p.onReceiveStatusLine = callback; | |
| } | |
| /** | |
| The content length in bytes when using request that has content | |
| e.g. POST/PUT and not using chunked transfer. Is set as the | |
| "Content-Length" header. Set to ulong.max to reset to chunked transfer. | |
| */ | |
| @property void contentLength(ulong len) | |
| { | |
| import std.conv : to; | |
| CurlOption lenOpt; | |
| // Force post if necessary | |
| if (p.method != Method.put && p.method != Method.post && | |
| p.method != Method.patch) | |
| p.method = Method.post; | |
| if (p.method == Method.post || p.method == Method.patch) | |
| lenOpt = CurlOption.postfieldsize_large; | |
| else | |
| lenOpt = CurlOption.infilesize_large; | |
| if (size_t.max != ulong.max && len == size_t.max) | |
| len = ulong.max; // check size_t.max for backwards compat, turn into error | |
| if (len == ulong.max) | |
| { | |
| // HTTP 1.1 supports requests with no length header set. | |
| addRequestHeader("Transfer-Encoding", "chunked"); | |
| addRequestHeader("Expect", "100-continue"); | |
| } | |
| else | |
| { | |
| p.curl.set(lenOpt, to!curl_off_t(len)); | |
| } | |
| } | |
| /** | |
| Authentication method as specified in $(LREF AuthMethod). | |
| */ | |
| @property void authenticationMethod(AuthMethod authMethod) | |
| { | |
| p.curl.set(CurlOption.httpauth, cast(long) authMethod); | |
| } | |
| /** | |
| Set max allowed redirections using the location header. | |
| uint.max for infinite. | |
| */ | |
| @property void maxRedirects(uint maxRedirs) | |
| { | |
| if (maxRedirs == uint.max) | |
| { | |
| // Disable | |
| p.curl.set(CurlOption.followlocation, 0); | |
| } | |
| else | |
| { | |
| p.curl.set(CurlOption.followlocation, 1); | |
| p.curl.set(CurlOption.maxredirs, maxRedirs); | |
| } | |
| } | |
| /** <a name="HTTP.Method"/>The standard HTTP methods : | |
| * $(HTTP www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1, _RFC2616 Section 5.1.1) | |
| */ | |
| enum Method | |
| { | |
| undefined, | |
| head, /// | |
| get, /// | |
| post, /// | |
| put, /// | |
| del, /// | |
| options, /// | |
| trace, /// | |
| connect, /// | |
| patch, /// | |
| } | |
| /** | |
| HTTP status line ie. the first line returned in an HTTP response. | |
| If authentication or redirections are done then the status will be for | |
| the last response received. | |
| */ | |
| struct StatusLine | |
| { | |
| ushort majorVersion; /// Major HTTP version ie. 1 in HTTP/1.0. | |
| ushort minorVersion; /// Minor HTTP version ie. 0 in HTTP/1.0. | |
| ushort code; /// HTTP status line code e.g. 200. | |
| string reason; /// HTTP status line reason string. | |
| /// Reset this status line | |
| @safe void reset() | |
| { | |
| majorVersion = 0; | |
| minorVersion = 0; | |
| code = 0; | |
| reason = ""; | |
| } | |
| /// | |
| string toString() const | |
| { | |
| import std.format : format; | |
| return format("%s %s (%s.%s)", | |
| code, reason, majorVersion, minorVersion); | |
| } | |
| } | |
| } // HTTP | |
| @system unittest // charset/Charset/CHARSET/... | |
| { | |
| import etc.c.curl; | |
| static foreach (c; ["charset", "Charset", "CHARSET", "CharSet", "charSet", | |
| "ChArSeT", "cHaRsEt"]) | |
| {{ | |
| testServer.handle((s) { | |
| s.send("HTTP/1.1 200 OK\r\n"~ | |
| "Content-Length: 0\r\n"~ | |
| "Content-Type: text/plain; " ~ c ~ "=foo\r\n" ~ | |
| "\r\n"); | |
| }); | |
| auto http = HTTP(testServer.addr); | |
| http.perform(); | |
| assert(http.p.charset == "foo"); | |
| // https://issues.dlang.org/show_bug.cgi?id=16736 | |
| double val; | |
| CurlCode code; | |
| code = http.getTiming(CurlInfo.total_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.namelookup_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.connect_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.pretransfer_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.starttransfer_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.redirect_time, val); | |
| assert(code == CurlError.ok); | |
| code = http.getTiming(CurlInfo.appconnect_time, val); | |
| assert(code == CurlError.ok); | |
| }} | |
| } | |
| /** | |
| FTP client functionality. | |
| See_Also: $(HTTP tools.ietf.org/html/rfc959, RFC959) | |
| */ | |
| struct FTP | |
| { | |
| mixin Protocol; | |
| import std.typecons : RefCounted; | |
| import etc.c.curl : CurlError, CurlInfo, curl_off_t, curl_slist; | |
| private struct Impl | |
| { | |
| ~this() | |
| { | |
| if (commands !is null) | |
| Curl.curl.slist_free_all(commands); | |
| if (curl.handle !is null) // work around RefCounted/emplace bug | |
| curl.shutdown(); | |
| } | |
| curl_slist* commands; | |
| Curl curl; | |
| string encoding; | |
| } | |
| private RefCounted!Impl p; | |
| /** | |
| FTP access to the specified url. | |
| */ | |
| static FTP opCall(const(char)[] url) | |
| { | |
| FTP ftp; | |
| ftp.initialize(); | |
| ftp.url = url; | |
| return ftp; | |
| } | |
| /// | |
| static FTP opCall() | |
| { | |
| FTP ftp; | |
| ftp.initialize(); | |
| return ftp; | |
| } | |
| /// | |
| FTP dup() | |
| { | |
| FTP copy = FTP(); | |
| copy.initialize(); | |
| copy.p.encoding = p.encoding; | |
| copy.p.curl = p.curl.dup(); | |
| curl_slist* cur = p.commands; | |
| curl_slist* newlist = null; | |
| while (cur) | |
| { | |
| newlist = Curl.curl.slist_append(newlist, cur.data); | |
| cur = cur.next; | |
| } | |
| copy.p.commands = newlist; | |
| copy.p.curl.set(CurlOption.postquote, copy.p.commands); | |
| copy.dataTimeout = _defaultDataTimeout; | |
| return copy; | |
| } | |
| private void initialize() | |
| { | |
| p.curl.initialize(); | |
| p.encoding = "ISO-8859-1"; | |
| dataTimeout = _defaultDataTimeout; | |
| } | |
| /** | |
| Performs the ftp request as it has been configured. | |
| After a FTP client has been setup and possibly assigned callbacks the $(D | |
| perform()) method will start performing the actual communication with the | |
| server. | |
| Params: | |
| throwOnError = whether to throw an exception or return a CurlCode on error | |
| */ | |
| CurlCode perform(ThrowOnError throwOnError = Yes.throwOnError) | |
| { | |
| return p.curl.perform(throwOnError); | |
| } | |
| /// The URL to specify the location of the resource. | |
| @property void url(const(char)[] url) | |
| { | |
| import std.algorithm.searching : startsWith; | |
| import std.uni : toLower; | |
| if (!startsWith(url.toLower(), "ftp://", "ftps://")) | |
| url = "ftp://" ~ url; | |
| p.curl.set(CurlOption.url, url); | |
| } | |
| // This is a workaround for mixed in content not having its | |
| // docs mixed in. | |
| version (StdDdoc) | |
| { | |
| static import etc.c.curl; | |
| /// Value to return from `onSend`/`onReceive` delegates in order to | |
| /// pause a request | |
| alias requestPause = CurlReadFunc.pause; | |
| /// Value to return from onSend delegate in order to abort a request | |
| alias requestAbort = CurlReadFunc.abort; | |
| /** | |
| True if the instance is stopped. A stopped instance is not usable. | |
| */ | |
| @property bool isStopped(); | |
| /// Stop and invalidate this instance. | |
| void shutdown(); | |
| /** Set verbose. | |
| This will print request information to stderr. | |
| */ | |
| @property void verbose(bool on); | |
| // Connection settings | |
| /// Set timeout for activity on connection. | |
| @property void dataTimeout(Duration d); | |
| /** Set maximum time an operation is allowed to take. | |
| This includes dns resolution, connecting, data transfer, etc. | |
| */ | |
| @property void operationTimeout(Duration d); | |
| /// Set timeout for connecting. | |
| @property void connectTimeout(Duration d); | |
| // Network settings | |
| /** Proxy | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy) | |
| */ | |
| @property void proxy(const(char)[] host); | |
| /** Proxy port | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXYPORT, _proxy_port) | |
| */ | |
| @property void proxyPort(ushort port); | |
| /// Type of proxy | |
| alias CurlProxy = etc.c.curl.CurlProxy; | |
| /** Proxy type | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy_type) | |
| */ | |
| @property void proxyType(CurlProxy type); | |
| /// DNS lookup timeout. | |
| @property void dnsTimeout(Duration d); | |
| /** | |
| * The network interface to use in form of the IP of the interface. | |
| * | |
| * Example: | |
| * ---- | |
| * theprotocol.netInterface = "192.168.1.32"; | |
| * theprotocol.netInterface = [ 192, 168, 1, 32 ]; | |
| * ---- | |
| * | |
| * See: $(REF InternetAddress, std,socket) | |
| */ | |
| @property void netInterface(const(char)[] i); | |
| /// ditto | |
| @property void netInterface(const(ubyte)[4] i); | |
| /// ditto | |
| @property void netInterface(InternetAddress i); | |
| /** | |
| Set the local outgoing port to use. | |
| Params: | |
| port = the first outgoing port number to try and use | |
| */ | |
| @property void localPort(ushort port); | |
| /** | |
| Set the local outgoing port range to use. | |
| This can be used together with the localPort property. | |
| Params: | |
| range = if the first port is occupied then try this many | |
| port number forwards | |
| */ | |
| @property void localPortRange(ushort range); | |
| /** Set the tcp no-delay socket option on or off. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTTCPNODELAY, nodelay) | |
| */ | |
| @property void tcpNoDelay(bool on); | |
| // Authentication settings | |
| /** | |
| Set the user name, password and optionally domain for authentication | |
| purposes. | |
| Some protocols may need authentication in some cases. Use this | |
| function to provide credentials. | |
| Params: | |
| username = the username | |
| password = the password | |
| domain = used for NTLM authentication only and is set to the NTLM domain | |
| name | |
| */ | |
| void setAuthentication(const(char)[] username, const(char)[] password, | |
| const(char)[] domain = ""); | |
| /** | |
| Set the user name and password for proxy authentication. | |
| Params: | |
| username = the username | |
| password = the password | |
| */ | |
| void setProxyAuthentication(const(char)[] username, const(char)[] password); | |
| /** | |
| * The event handler that gets called when data is needed for sending. The | |
| * length of the `void[]` specifies the maximum number of bytes that can | |
| * be sent. | |
| * | |
| * Returns: | |
| * The callback returns the number of elements in the buffer that have been | |
| * filled and are ready to send. | |
| * The special value `.abortRequest` can be returned in order to abort the | |
| * current request. | |
| * The special value `.pauseRequest` can be returned in order to pause the | |
| * current request. | |
| * | |
| */ | |
| @property void onSend(size_t delegate(void[]) callback); | |
| /** | |
| * The event handler that receives incoming data. Be sure to copy the | |
| * incoming ubyte[] since it is not guaranteed to be valid after the | |
| * callback returns. | |
| * | |
| * Returns: | |
| * The callback returns the incoming bytes read. If not the entire array is | |
| * the request will abort. | |
| * The special value .pauseRequest can be returned in order to pause the | |
| * current request. | |
| * | |
| */ | |
| @property void onReceive(size_t delegate(ubyte[]) callback); | |
| /** | |
| * The event handler that gets called to inform of upload/download progress. | |
| * | |
| * Callback_parameters: | |
| * $(CALLBACK_PARAMS) | |
| * | |
| * Callback_returns: | |
| * Return 0 from the callback to signal success, return non-zero to | |
| * abort transfer. | |
| */ | |
| @property void onProgress(int delegate(size_t dlTotal, size_t dlNow, | |
| size_t ulTotal, size_t ulNow) callback); | |
| } | |
| /** Clear all commands send to ftp server. | |
| */ | |
| void clearCommands() | |
| { | |
| if (p.commands !is null) | |
| Curl.curl.slist_free_all(p.commands); | |
| p.commands = null; | |
| p.curl.clear(CurlOption.postquote); | |
| } | |
| /** Add a command to send to ftp server. | |
| * | |
| * There is no remove command functionality. Do a $(LREF clearCommands) and | |
| * set the needed commands instead. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * auto client = FTP(); | |
| * client.addCommand("RNFR my_file.txt"); | |
| * client.addCommand("RNTO my_renamed_file.txt"); | |
| * upload("my_file.txt", "ftp.digitalmars.com", client); | |
| * --- | |
| */ | |
| void addCommand(const(char)[] command) | |
| { | |
| import std.internal.cstring : tempCString; | |
| p.commands = Curl.curl.slist_append(p.commands, | |
| command.tempCString().buffPtr); | |
| p.curl.set(CurlOption.postquote, p.commands); | |
| } | |
| /// Connection encoding. Defaults to ISO-8859-1. | |
| @property void encoding(string name) | |
| { | |
| p.encoding = name; | |
| } | |
| /// ditto | |
| @property string encoding() | |
| { | |
| return p.encoding; | |
| } | |
| /** | |
| The content length in bytes of the ftp data. | |
| */ | |
| @property void contentLength(ulong len) | |
| { | |
| import std.conv : to; | |
| p.curl.set(CurlOption.infilesize_large, to!curl_off_t(len)); | |
| } | |
| /** | |
| * Get various timings defined in $(REF CurlInfo, etc, c, curl). | |
| * The value is usable only if the return value is equal to `etc.c.curl.CurlError.ok`. | |
| * | |
| * Params: | |
| * timing = one of the timings defined in $(REF CurlInfo, etc, c, curl). | |
| * The values are: | |
| * `etc.c.curl.CurlInfo.namelookup_time`, | |
| * `etc.c.curl.CurlInfo.connect_time`, | |
| * `etc.c.curl.CurlInfo.pretransfer_time`, | |
| * `etc.c.curl.CurlInfo.starttransfer_time`, | |
| * `etc.c.curl.CurlInfo.redirect_time`, | |
| * `etc.c.curl.CurlInfo.appconnect_time`, | |
| * `etc.c.curl.CurlInfo.total_time`. | |
| * val = the actual value of the inquired timing. | |
| * | |
| * Returns: | |
| * The return code of the operation. The value stored in val | |
| * should be used only if the return value is `etc.c.curl.CurlInfo.ok`. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * import etc.c.curl : CurlError, CurlInfo; | |
| * | |
| * auto client = FTP(); | |
| * client.addCommand("RNFR my_file.txt"); | |
| * client.addCommand("RNTO my_renamed_file.txt"); | |
| * upload("my_file.txt", "ftp.digitalmars.com", client); | |
| * | |
| * double val; | |
| * CurlCode code; | |
| * | |
| * code = client.getTiming(CurlInfo.namelookup_time, val); | |
| * assert(code == CurlError.ok); | |
| * --- | |
| */ | |
| CurlCode getTiming(CurlInfo timing, ref double val) | |
| { | |
| return p.curl.getTiming(timing, val); | |
| } | |
| @system unittest | |
| { | |
| auto client = FTP(); | |
| double val; | |
| CurlCode code; | |
| code = client.getTiming(CurlInfo.total_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.namelookup_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.connect_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.pretransfer_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.starttransfer_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.redirect_time, val); | |
| assert(code == CurlError.ok); | |
| code = client.getTiming(CurlInfo.appconnect_time, val); | |
| assert(code == CurlError.ok); | |
| } | |
| } | |
| /** | |
| * Basic SMTP protocol support. | |
| * | |
| * Example: | |
| * --- | |
| * import std.net.curl; | |
| * | |
| * // Send an email with SMTPS | |
| * auto smtp = SMTP("smtps://smtp.gmail.com"); | |
| * smtp.setAuthentication("from.addr@gmail.com", "password"); | |
| * smtp.mailTo = ["<to.addr@gmail.com>"]; | |
| * smtp.mailFrom = "<from.addr@gmail.com>"; | |
| * smtp.message = "Example Message"; | |
| * smtp.perform(); | |
| * --- | |
| * | |
| * See_Also: $(HTTP www.ietf.org/rfc/rfc2821.txt, RFC2821) | |
| */ | |
| struct SMTP | |
| { | |
| mixin Protocol; | |
| import std.typecons : RefCounted; | |
| import etc.c.curl : CurlUseSSL, curl_slist; | |
| private struct Impl | |
| { | |
| ~this() | |
| { | |
| if (curl.handle !is null) // work around RefCounted/emplace bug | |
| curl.shutdown(); | |
| } | |
| Curl curl; | |
| @property void message(string msg) | |
| { | |
| import std.algorithm.comparison : min; | |
| auto _message = msg; | |
| /** | |
| This delegate reads the message text and copies it. | |
| */ | |
| curl.onSend = delegate size_t(void[] data) | |
| { | |
| if (!msg.length) return 0; | |
| size_t to_copy = min(data.length, _message.length); | |
| data[0 .. to_copy] = (cast(void[])_message)[0 .. to_copy]; | |
| _message = _message[to_copy..$]; | |
| return to_copy; | |
| }; | |
| } | |
| } | |
| private RefCounted!Impl p; | |
| /** | |
| Sets to the URL of the SMTP server. | |
| */ | |
| static SMTP opCall(const(char)[] url) | |
| { | |
| SMTP smtp; | |
| smtp.initialize(); | |
| smtp.url = url; | |
| return smtp; | |
| } | |
| /// | |
| static SMTP opCall() | |
| { | |
| SMTP smtp; | |
| smtp.initialize(); | |
| return smtp; | |
| } | |
| /+ TODO: The other structs have this function. | |
| SMTP dup() | |
| { | |
| SMTP copy = SMTP(); | |
| copy.initialize(); | |
| copy.p.encoding = p.encoding; | |
| copy.p.curl = p.curl.dup(); | |
| curl_slist* cur = p.commands; | |
| curl_slist* newlist = null; | |
| while (cur) | |
| { | |
| newlist = Curl.curl.slist_append(newlist, cur.data); | |
| cur = cur.next; | |
| } | |
| copy.p.commands = newlist; | |
| copy.p.curl.set(CurlOption.postquote, copy.p.commands); | |
| copy.dataTimeout = _defaultDataTimeout; | |
| return copy; | |
| } | |
| +/ | |
| /** | |
| Performs the request as configured. | |
| Params: | |
| throwOnError = whether to throw an exception or return a CurlCode on error | |
| */ | |
| CurlCode perform(ThrowOnError throwOnError = Yes.throwOnError) | |
| { | |
| return p.curl.perform(throwOnError); | |
| } | |
| /// The URL to specify the location of the resource. | |
| @property void url(const(char)[] url) | |
| { | |
| import std.algorithm.searching : startsWith; | |
| import std.exception : enforce; | |
| import std.uni : toLower; | |
| auto lowered = url.toLower(); | |
| if (lowered.startsWith("smtps://")) | |
| { | |
| p.curl.set(CurlOption.use_ssl, CurlUseSSL.all); | |
| } | |
| else | |
| { | |
| enforce!CurlException(lowered.startsWith("smtp://"), | |
| "The url must be for the smtp protocol."); | |
| } | |
| p.curl.set(CurlOption.url, url); | |
| } | |
| private void initialize() | |
| { | |
| p.curl.initialize(); | |
| p.curl.set(CurlOption.upload, 1L); | |
| dataTimeout = _defaultDataTimeout; | |
| verifyPeer = true; | |
| verifyHost = true; | |
| } | |
| // This is a workaround for mixed in content not having its | |
| // docs mixed in. | |
| version (StdDdoc) | |
| { | |
| static import etc.c.curl; | |
| /// Value to return from `onSend`/`onReceive` delegates in order to | |
| /// pause a request | |
| alias requestPause = CurlReadFunc.pause; | |
| /// Value to return from onSend delegate in order to abort a request | |
| alias requestAbort = CurlReadFunc.abort; | |
| /** | |
| True if the instance is stopped. A stopped instance is not usable. | |
| */ | |
| @property bool isStopped(); | |
| /// Stop and invalidate this instance. | |
| void shutdown(); | |
| /** Set verbose. | |
| This will print request information to stderr. | |
| */ | |
| @property void verbose(bool on); | |
| // Connection settings | |
| /// Set timeout for activity on connection. | |
| @property void dataTimeout(Duration d); | |
| /** Set maximum time an operation is allowed to take. | |
| This includes dns resolution, connecting, data transfer, etc. | |
| */ | |
| @property void operationTimeout(Duration d); | |
| /// Set timeout for connecting. | |
| @property void connectTimeout(Duration d); | |
| // Network settings | |
| /** Proxy | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy) | |
| */ | |
| @property void proxy(const(char)[] host); | |
| /** Proxy port | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXYPORT, _proxy_port) | |
| */ | |
| @property void proxyPort(ushort port); | |
| /// Type of proxy | |
| alias CurlProxy = etc.c.curl.CurlProxy; | |
| /** Proxy type | |
| * See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY, _proxy_type) | |
| */ | |
| @property void proxyType(CurlProxy type); | |
| /// DNS lookup timeout. | |
| @property void dnsTimeout(Duration d); | |
| /** | |
| * The network interface to use in form of the IP of the interface. | |
| * | |
| * Example: | |
| * ---- | |
| * theprotocol.netInterface = "192.168.1.32"; | |
| * theprotocol.netInterface = [ 192, 168, 1, 32 ]; | |
| * ---- | |
| * | |
| * See: $(REF InternetAddress, std,socket) | |
| */ | |
| @property void netInterface(const(char)[] i); | |
| /// ditto | |
| @property void netInterface(const(ubyte)[4] i); | |
| /// ditto | |
| @property void netInterface(InternetAddress i); | |
| /** | |
| Set the local outgoing port to use. | |
| Params: | |
| port = the first outgoing port number to try and use | |
| */ | |
| @property void localPort(ushort port); | |
| /** | |
| Set the local outgoing port range to use. | |
| This can be used together with the localPort property. | |
| Params: | |
| range = if the first port is occupied then try this many | |
| port number forwards | |
| */ | |
| @property void localPortRange(ushort range); | |
| /** Set the tcp no-delay socket option on or off. | |
| See: $(HTTP curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTTCPNODELAY, nodelay) | |
| */ | |
| @property void tcpNoDelay(bool on); | |
| // Authentication settings | |
| /** | |
| Set the user name, password and optionally domain for authentication | |
| purposes. | |
| Some protocols may need authentication in some cases. Use this | |
| function to provide credentials. | |
| Params: | |
| username = the username | |
| password = the password | |
| domain = used for NTLM authentication only and is set to the NTLM domain | |
| name | |
| */ | |
| void setAuthentication(const(char)[] username, const(char)[] password, | |
| const(char)[] domain = ""); | |
| /** | |
| Set the user name and password for proxy authentication. | |
| Params: | |
| username = the username | |
| password = the password | |
| */ | |
| void setProxyAuthentication(const(char)[] username, const(char)[] password); | |
| /** | |
| * The event handler that gets called when data is needed for sending. The | |
| * length of the `void[]` specifies the maximum number of bytes that can | |
| * be sent. | |
| * | |
| * Returns: | |
| * The callback returns the number of elements in the buffer that have been | |
| * filled and are ready to send. | |
| * The special value `.abortRequest` can be returned in order to abort the | |
| * current request. | |
| * The special value `.pauseRequest` can be returned in order to pause the | |
| * current request. | |
| */ | |
| @property void onSend(size_t delegate(void[]) callback); | |
| /** | |
| * The event handler that receives incoming data. Be sure to copy the | |
| * incoming ubyte[] since it is not guaranteed to be valid after the | |
| * callback returns. | |
| * | |
| * Returns: | |
| * The callback returns the incoming bytes read. If not the entire array is | |
| * the request will abort. | |
| * The special value .pauseRequest can be returned in order to pause the | |
| * current request. | |
| */ | |
| @property void onReceive(size_t delegate(ubyte[]) callback); | |
| /** | |
| * The event handler that gets called to inform of upload/download progress. | |
| * | |
| * Callback_parameters: | |
| * $(CALLBACK_PARAMS) | |
| * | |
| * Callback_returns: | |
| * Return 0 from the callback to signal success, return non-zero to | |
| * abort transfer. | |
| */ | |
| @property void onProgress(int delegate(size_t dlTotal, size_t dlNow, | |
| size_t ulTotal, size_t ulNow) callback); | |
| } | |
| /** | |
| Setter for the sender's email address. | |
| */ | |
| @property void mailFrom()(const(char)[] sender) | |
| { | |
| assert(!sender.empty, "Sender must not be empty"); | |
| p.curl.set(CurlOption.mail_from, sender); | |
| } | |
| /** | |
| Setter for the recipient email addresses. | |
| */ | |
| void mailTo()(const(char)[][] recipients...) | |
| { | |
| import std.internal.cstring : tempCString; | |
| assert(!recipients.empty, "Recipient must not be empty"); | |
| curl_slist* recipients_list = null; | |
| foreach (recipient; recipients) | |
| { | |
| recipients_list = | |
| Curl.curl.slist_append(recipients_list, | |
| recipient.tempCString().buffPtr); | |
| } | |
| p.curl.set(CurlOption.mail_rcpt, recipients_list); | |
| } | |
| /** | |
| Sets the message body text. | |
| */ | |
| @property void message(string msg) | |
| { | |
| p.message = msg; | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.net.curl; | |
| // Send an email with SMTPS | |
| auto smtp = SMTP("smtps://smtp.gmail.com"); | |
| smtp.setAuthentication("from.addr@gmail.com", "password"); | |
| smtp.mailTo = ["<to.addr@gmail.com>"]; | |
| smtp.mailFrom = "<from.addr@gmail.com>"; | |
| smtp.message = "Example Message"; | |
| //smtp.perform(); | |
| } | |
| /++ | |
| Exception thrown on errors in std.net.curl functions. | |
| +/ | |
| class CurlException : Exception | |
| { | |
| /++ | |
| Params: | |
| msg = The message for the exception. | |
| file = The file where the exception occurred. | |
| line = The line number where the exception occurred. | |
| next = The previous exception in the chain of exceptions, if any. | |
| +/ | |
| @safe pure nothrow | |
| this(string msg, | |
| string file = __FILE__, | |
| size_t line = __LINE__, | |
| Throwable next = null) | |
| { | |
| super(msg, file, line, next); | |
| } | |
| } | |
| /++ | |
| Exception thrown on timeout errors in std.net.curl functions. | |
| +/ | |
| class CurlTimeoutException : CurlException | |
| { | |
| /++ | |
| Params: | |
| msg = The message for the exception. | |
| file = The file where the exception occurred. | |
| line = The line number where the exception occurred. | |
| next = The previous exception in the chain of exceptions, if any. | |
| +/ | |
| @safe pure nothrow | |
| this(string msg, | |
| string file = __FILE__, | |
| size_t line = __LINE__, | |
| Throwable next = null) | |
| { | |
| super(msg, file, line, next); | |
| } | |
| } | |
| /++ | |
| Exception thrown on HTTP request failures, e.g. 404 Not Found. | |
| +/ | |
| class HTTPStatusException : CurlException | |
| { | |
| /++ | |
| Params: | |
| status = The HTTP status code. | |
| msg = The message for the exception. | |
| file = The file where the exception occurred. | |
| line = The line number where the exception occurred. | |
| next = The previous exception in the chain of exceptions, if any. | |
| +/ | |
| @safe pure nothrow | |
| this(int status, | |
| string msg, | |
| string file = __FILE__, | |
| size_t line = __LINE__, | |
| Throwable next = null) | |
| { | |
| super(msg, file, line, next); | |
| this.status = status; | |
| } | |
| immutable int status; /// The HTTP status code | |
| } | |
| /// Equal to $(REF CURLcode, etc,c,curl) | |
| alias CurlCode = CURLcode; | |
| /// Flag to specify whether or not an exception is thrown on error. | |
| alias ThrowOnError = Flag!"throwOnError"; | |
| private struct CurlAPI | |
| { | |
| import etc.c.curl : CurlGlobal; | |
| static struct API | |
| { | |
| import etc.c.curl : curl_version_info, curl_version_info_data, | |
| CURL, CURLcode, CURLINFO, CURLoption, CURLversion, curl_slist; | |
| extern(C): | |
| import core.stdc.config : c_long; | |
| CURLcode function(c_long flags) global_init; | |
| void function() global_cleanup; | |
| curl_version_info_data * function(CURLversion) version_info; | |
| CURL* function() easy_init; | |
| CURLcode function(CURL *curl, CURLoption option,...) easy_setopt; | |
| CURLcode function(CURL *curl) easy_perform; | |
| CURLcode function(CURL *curl, CURLINFO info,...) easy_getinfo; | |
| CURL* function(CURL *curl) easy_duphandle; | |
| char* function(CURLcode) easy_strerror; | |
| CURLcode function(CURL *handle, int bitmask) easy_pause; | |
| void function(CURL *curl) easy_cleanup; | |
| curl_slist* function(curl_slist *, char *) slist_append; | |
| void function(curl_slist *) slist_free_all; | |
| } | |
| __gshared API _api; | |
| __gshared void* _handle; | |
| static ref API instance() @property | |
| { | |
| import std.concurrency : initOnce; | |
| initOnce!_handle(loadAPI()); | |
| return _api; | |
| } | |
| static void* loadAPI() | |
| { | |
| import std.exception : enforce; | |
| version (Posix) | |
| { | |
| import core.sys.posix.dlfcn : dlsym, dlopen, dlclose, RTLD_LAZY; | |
| alias loadSym = dlsym; | |
| } | |
| else version (Windows) | |
| { | |
| import core.sys.windows.winbase : GetProcAddress, GetModuleHandleA, | |
| LoadLibraryA; | |
| alias loadSym = GetProcAddress; | |
| } | |
| else | |
| static assert(0, "unimplemented"); | |
| void* handle; | |
| version (Posix) | |
| handle = dlopen(null, RTLD_LAZY); | |
| else version (Windows) | |
| handle = GetModuleHandleA(null); | |
| assert(handle !is null); | |
| // try to load curl from the executable to allow static linking | |
| if (loadSym(handle, "curl_global_init") is null) | |
| { | |
| import std.format : format; | |
| version (Posix) | |
| dlclose(handle); | |
| version (LibcurlPath) | |
| { | |
| import std.string : strip; | |
| static immutable names = [strip(import("LibcurlPathFile"))]; | |
| } | |
| else version (OSX) | |
| static immutable names = ["libcurl.4.dylib"]; | |
| else version (Posix) | |
| { | |
| static immutable names = ["libcurl.so", "libcurl.so.4", | |
| "libcurl-gnutls.so.4", "libcurl-nss.so.4", "libcurl.so.3"]; | |
| } | |
| else version (Windows) | |
| static immutable names = ["libcurl.dll", "curl.dll"]; | |
| foreach (name; names) | |
| { | |
| version (Posix) | |
| handle = dlopen(name.ptr, RTLD_LAZY); | |
| else version (Windows) | |
| handle = LoadLibraryA(name.ptr); | |
| if (handle !is null) break; | |
| } | |
| enforce!CurlException(handle !is null, "Failed to load curl, tried %(%s, %).".format(names)); | |
| } | |
| foreach (i, FP; typeof(API.tupleof)) | |
| { | |
| enum name = __traits(identifier, _api.tupleof[i]); | |
| auto p = enforce!CurlException(loadSym(handle, "curl_"~name), | |
| "Couldn't load curl_"~name~" from libcurl."); | |
| _api.tupleof[i] = cast(FP) p; | |
| } | |
| enforce!CurlException(!_api.global_init(CurlGlobal.all), | |
| "Failed to initialize libcurl"); | |
| static extern(C) void cleanup() | |
| { | |
| if (_handle is null) return; | |
| _api.global_cleanup(); | |
| version (Posix) | |
| { | |
| import core.sys.posix.dlfcn : dlclose; | |
| dlclose(_handle); | |
| } | |
| else version (Windows) | |
| { | |
| import core.sys.windows.winbase : FreeLibrary; | |
| FreeLibrary(_handle); | |
| } | |
| else | |
| static assert(0, "unimplemented"); | |
| _api = API.init; | |
| _handle = null; | |
| } | |
| import core.stdc.stdlib : atexit; | |
| atexit(&cleanup); | |
| return handle; | |
| } | |
| } | |
| /** | |
| Wrapper to provide a better interface to libcurl than using the plain C API. | |
| It is recommended to use the `HTTP`/`FTP` etc. structs instead unless | |
| raw access to libcurl is needed. | |
| Warning: This struct uses interior pointers for callbacks. Only allocate it | |
| on the stack if you never move or copy it. This also means passing by reference | |
| when passing Curl to other functions. Otherwise always allocate on | |
| the heap. | |
| */ | |
| struct Curl | |
| { | |
| import etc.c.curl : CURL, CurlError, CurlPause, CurlSeek, CurlSeekPos, | |
| curl_socket_t, CurlSockType, | |
| CurlReadFunc, CurlInfo, curlsocktype, curl_off_t, | |
| LIBCURL_VERSION_MAJOR, LIBCURL_VERSION_MINOR, LIBCURL_VERSION_PATCH; | |
| alias OutData = void[]; | |
| alias InData = ubyte[]; | |
| private bool _stopped; | |
| private static auto ref curl() @property { return CurlAPI.instance; } | |
| // A handle should not be used by two threads simultaneously | |
| private CURL* handle; | |
| // May also return `CURL_READFUNC_ABORT` or `CURL_READFUNC_PAUSE` | |
| private size_t delegate(OutData) _onSend; | |
| private size_t delegate(InData) _onReceive; | |
| private void delegate(in char[]) _onReceiveHeader; | |
| private CurlSeek delegate(long,CurlSeekPos) _onSeek; | |
| private int delegate(curl_socket_t,CurlSockType) _onSocketOption; | |
| private int delegate(size_t dltotal, size_t dlnow, | |
| size_t ultotal, size_t ulnow) _onProgress; | |
| alias requestPause = CurlReadFunc.pause; | |
| alias requestAbort = CurlReadFunc.abort; | |
| /** | |
| Initialize the instance by creating a working curl handle. | |
| */ | |
| void initialize() | |
| { | |
| import std.exception : enforce; | |
| enforce!CurlException(!handle, "Curl instance already initialized"); | |
| handle = curl.easy_init(); | |
| enforce!CurlException(handle, "Curl instance couldn't be initialized"); | |
| _stopped = false; | |
| set(CurlOption.nosignal, 1); | |
| } | |
| /// | |
| @property bool stopped() const | |
| { | |
| return _stopped; | |
| } | |
| /** | |
| Duplicate this handle. | |
| The new handle will have all options set as the one it was duplicated | |
| from. An exception to this is that all options that cannot be shared | |
| across threads are reset thereby making it safe to use the duplicate | |
| in a new thread. | |
| */ | |
| Curl dup() | |
| { | |
| import std.meta : AliasSeq; | |
| Curl copy; | |
| copy.handle = curl.easy_duphandle(handle); | |
| copy._stopped = false; | |
| with (CurlOption) { | |
| auto tt = AliasSeq!(file, writefunction, writeheader, | |
| headerfunction, infile, readfunction, ioctldata, ioctlfunction, | |
| seekdata, seekfunction, sockoptdata, sockoptfunction, | |
| opensocketdata, opensocketfunction, progressdata, | |
| progressfunction, debugdata, debugfunction, interleavedata, | |
| interleavefunction, chunk_data, chunk_bgn_function, | |
| chunk_end_function, fnmatch_data, fnmatch_function, cookiejar, postfields); | |
| foreach (option; tt) | |
| copy.clear(option); | |
| } | |
| // The options are only supported by libcurl when it has been built | |
| // against certain versions of OpenSSL - if your libcurl uses an old | |
| // OpenSSL, or uses an entirely different SSL engine, attempting to | |
| // clear these normally will raise an exception | |
| copy.clearIfSupported(CurlOption.ssl_ctx_function); | |
| copy.clearIfSupported(CurlOption.ssh_keydata); | |
| // Enable for curl version > 7.21.7 | |
| static if (LIBCURL_VERSION_MAJOR >= 7 && | |
| LIBCURL_VERSION_MINOR >= 21 && | |
| LIBCURL_VERSION_PATCH >= 7) | |
| { | |
| copy.clear(CurlOption.closesocketdata); | |
| copy.clear(CurlOption.closesocketfunction); | |
| } | |
| copy.set(CurlOption.nosignal, 1); | |
| // copy.clear(CurlOption.ssl_ctx_data); Let ssl function be shared | |
| // copy.clear(CurlOption.ssh_keyfunction); Let key function be shared | |
| /* | |
| Allow sharing of conv functions | |
| copy.clear(CurlOption.conv_to_network_function); | |
| copy.clear(CurlOption.conv_from_network_function); | |
| copy.clear(CurlOption.conv_from_utf8_function); | |
| */ | |
| return copy; | |
| } | |
| private void _check(CurlCode code) | |
| { | |
| import std.exception : enforce; | |
| enforce!CurlTimeoutException(code != CurlError.operation_timedout, | |
| errorString(code)); | |
| enforce!CurlException(code == CurlError.ok, | |
| errorString(code)); | |
| } | |
| private string errorString(CurlCode code) | |
| { | |
| import core.stdc.string : strlen; | |
| import std.format : format; | |
| auto msgZ = curl.easy_strerror(code); | |
| // doing the following (instead of just using std.conv.to!string) avoids 1 allocation | |
| return format("%s on handle %s", msgZ[0 .. strlen(msgZ)], handle); | |
| } | |
| private void throwOnStopped(string message = null) | |
| { | |
| import std.exception : enforce; | |
| auto def = "Curl instance called after being cleaned up"; | |
| enforce!CurlException(!stopped, | |
| message == null ? def : message); | |
| } | |
| /** | |
| Stop and invalidate this curl instance. | |
| Warning: Do not call this from inside a callback handler e.g. `onReceive`. | |
| */ | |
| void shutdown() | |
| { | |
| throwOnStopped(); | |
| _stopped = true; | |
| curl.easy_cleanup(this.handle); | |
| this.handle = null; | |
| } | |
| /** | |
| Pausing and continuing transfers. | |
| */ | |
| void pause(bool sendingPaused, bool receivingPaused) | |
| { | |
| throwOnStopped(); | |
| _check(curl.easy_pause(this.handle, | |
| (sendingPaused ? CurlPause.send_cont : CurlPause.send) | | |
| (receivingPaused ? CurlPause.recv_cont : CurlPause.recv))); | |
| } | |
| /** | |
| Set a string curl option. | |
| Params: | |
| option = A $(REF CurlOption, etc,c,curl) as found in the curl documentation | |
| value = The string | |
| */ | |
| void set(CurlOption option, const(char)[] value) | |
| { | |
| import std.internal.cstring : tempCString; | |
| throwOnStopped(); | |
| _check(curl.easy_setopt(this.handle, option, value.tempCString().buffPtr)); | |
| } | |
| /** | |
| Set a long curl option. | |
| Params: | |
| option = A $(REF CurlOption, etc,c,curl) as found in the curl documentation | |
| value = The long | |
| */ | |
| void set(CurlOption option, long value) | |
| { | |
| throwOnStopped(); | |
| _check(curl.easy_setopt(this.handle, option, value)); | |
| } | |
| /** | |
| Set a void* curl option. | |
| Params: | |
| option = A $(REF CurlOption, etc,c,curl) as found in the curl documentation | |
| value = The pointer | |
| */ | |
| void set(CurlOption option, void* value) | |
| { | |
| throwOnStopped(); | |
| _check(curl.easy_setopt(this.handle, option, value)); | |
| } | |
| /** | |
| Clear a pointer option. | |
| Params: | |
| option = A $(REF CurlOption, etc,c,curl) as found in the curl documentation | |
| */ | |
| void clear(CurlOption option) | |
| { | |
| throwOnStopped(); | |
| _check(curl.easy_setopt(this.handle, option, null)); | |
| } | |
| /** | |
| Clear a pointer option. Does not raise an exception if the underlying | |
| libcurl does not support the option. Use sparingly. | |
| Params: | |
| option = A $(REF CurlOption, etc,c,curl) as found in the curl documentation | |
| */ | |
| void clearIfSupported(CurlOption option) | |
| { | |
| throwOnStopped(); | |
| auto rval = curl.easy_setopt(this.handle, option, null); | |
| if (rval != CurlError.unknown_option && rval != CurlError.not_built_in) | |
| _check(rval); | |
| } | |
| /** | |
| perform the curl request by doing the HTTP,FTP etc. as it has | |
| been setup beforehand. | |
| Params: | |
| throwOnError = whether to throw an exception or return a CurlCode on error | |
| */ | |
| CurlCode perform(ThrowOnError throwOnError = Yes.throwOnError) | |
| { | |
| throwOnStopped(); | |
| CurlCode code = curl.easy_perform(this.handle); | |
| if (throwOnError) | |
| _check(code); | |
| return code; | |
| } | |
| /** | |
| Get the various timings like name lookup time, total time, connect time etc. | |
| The timed category is passed through the timing parameter while the timing | |
| value is stored at val. The value is usable only if res is equal to | |
| `etc.c.curl.CurlError.ok`. | |
| */ | |
| CurlCode getTiming(CurlInfo timing, ref double val) | |
| { | |
| CurlCode code; | |
| code = curl.easy_getinfo(handle, timing, &val); | |
| return code; | |
| } | |
| /** | |
| * The event handler that receives incoming data. | |
| * | |
| * Params: | |
| * callback = the callback that receives the `ubyte[]` data. | |
| * Be sure to copy the incoming data and not store | |
| * a slice. | |
| * | |
| * Returns: | |
| * The callback returns the incoming bytes read. If not the entire array is | |
| * the request will abort. | |
| * The special value HTTP.pauseRequest can be returned in order to pause the | |
| * current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio, std.conv; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * curl.onReceive = (ubyte[] data) { writeln("Got data", to!(const(char)[])(data)); return data.length;}; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onReceive(size_t delegate(InData) callback) | |
| { | |
| _onReceive = (InData id) | |
| { | |
| throwOnStopped("Receive callback called on cleaned up Curl instance"); | |
| return callback(id); | |
| }; | |
| set(CurlOption.file, cast(void*) &this); | |
| set(CurlOption.writefunction, cast(void*) &Curl._receiveCallback); | |
| } | |
| /** | |
| * The event handler that receives incoming headers for protocols | |
| * that uses headers. | |
| * | |
| * Params: | |
| * callback = the callback that receives the header string. | |
| * Make sure the callback copies the incoming params if | |
| * it needs to store it because they are references into | |
| * the backend and may very likely change. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * curl.onReceiveHeader = (in char[] header) { writeln(header); }; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onReceiveHeader(void delegate(in char[]) callback) | |
| { | |
| _onReceiveHeader = (in char[] od) | |
| { | |
| throwOnStopped("Receive header callback called on "~ | |
| "cleaned up Curl instance"); | |
| callback(od); | |
| }; | |
| set(CurlOption.writeheader, cast(void*) &this); | |
| set(CurlOption.headerfunction, | |
| cast(void*) &Curl._receiveHeaderCallback); | |
| } | |
| /** | |
| * The event handler that gets called when data is needed for sending. | |
| * | |
| * Params: | |
| * callback = the callback that has a `void[]` buffer to be filled | |
| * | |
| * Returns: | |
| * The callback returns the number of elements in the buffer that have been | |
| * filled and are ready to send. | |
| * The special value `Curl.abortRequest` can be returned in | |
| * order to abort the current request. | |
| * The special value `Curl.pauseRequest` can be returned in order to | |
| * pause the current request. | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * | |
| * string msg = "Hello world"; | |
| * curl.onSend = (void[] data) | |
| * { | |
| * auto m = cast(void[]) msg; | |
| * size_t length = m.length > data.length ? data.length : m.length; | |
| * if (length == 0) return 0; | |
| * data[0 .. length] = m[0 .. length]; | |
| * msg = msg[length..$]; | |
| * return length; | |
| * }; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onSend(size_t delegate(OutData) callback) | |
| { | |
| _onSend = (OutData od) | |
| { | |
| throwOnStopped("Send callback called on cleaned up Curl instance"); | |
| return callback(od); | |
| }; | |
| set(CurlOption.infile, cast(void*) &this); | |
| set(CurlOption.readfunction, cast(void*) &Curl._sendCallback); | |
| } | |
| /** | |
| * The event handler that gets called when the curl backend needs to seek | |
| * the data to be sent. | |
| * | |
| * Params: | |
| * callback = the callback that receives a seek offset and a seek position | |
| * $(REF CurlSeekPos, etc,c,curl) | |
| * | |
| * Returns: | |
| * The callback returns the success state of the seeking | |
| * $(REF CurlSeek, etc,c,curl) | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * curl.onSeek = (long p, CurlSeekPos sp) | |
| * { | |
| * return CurlSeek.cantseek; | |
| * }; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onSeek(CurlSeek delegate(long, CurlSeekPos) callback) | |
| { | |
| _onSeek = (long ofs, CurlSeekPos sp) | |
| { | |
| throwOnStopped("Seek callback called on cleaned up Curl instance"); | |
| return callback(ofs, sp); | |
| }; | |
| set(CurlOption.seekdata, cast(void*) &this); | |
| set(CurlOption.seekfunction, cast(void*) &Curl._seekCallback); | |
| } | |
| /** | |
| * The event handler that gets called when the net socket has been created | |
| * but a `connect()` call has not yet been done. This makes it possible to set | |
| * misc. socket options. | |
| * | |
| * Params: | |
| * callback = the callback that receives the socket and socket type | |
| * $(REF CurlSockType, etc,c,curl) | |
| * | |
| * Returns: | |
| * Return 0 from the callback to signal success, return 1 to signal error | |
| * and make curl close the socket | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * curl.onSocketOption = delegate int(curl_socket_t s, CurlSockType t) { /+ do stuff +/ }; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onSocketOption(int delegate(curl_socket_t, | |
| CurlSockType) callback) | |
| { | |
| _onSocketOption = (curl_socket_t sock, CurlSockType st) | |
| { | |
| throwOnStopped("Socket option callback called on "~ | |
| "cleaned up Curl instance"); | |
| return callback(sock, st); | |
| }; | |
| set(CurlOption.sockoptdata, cast(void*) &this); | |
| set(CurlOption.sockoptfunction, | |
| cast(void*) &Curl._socketOptionCallback); | |
| } | |
| /** | |
| * The event handler that gets called to inform of upload/download progress. | |
| * | |
| * Params: | |
| * callback = the callback that receives the (total bytes to download, | |
| * currently downloaded bytes, total bytes to upload, currently uploaded | |
| * bytes). | |
| * | |
| * Returns: | |
| * Return 0 from the callback to signal success, return non-zero to abort | |
| * transfer | |
| * | |
| * Example: | |
| * ---- | |
| * import std.net.curl, std.stdio; | |
| * Curl curl; | |
| * curl.initialize(); | |
| * curl.set(CurlOption.url, "http://dlang.org"); | |
| * curl.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) | |
| * { | |
| * writeln("Progress: downloaded bytes ", dlnow, " of ", dltotal); | |
| * writeln("Progress: uploaded bytes ", ulnow, " of ", ultotal); | |
| * return 0; | |
| * }; | |
| * curl.perform(); | |
| * ---- | |
| */ | |
| @property void onProgress(int delegate(size_t dlTotal, | |
| size_t dlNow, | |
| size_t ulTotal, | |
| size_t ulNow) callback) | |
| { | |
| _onProgress = (size_t dlt, size_t dln, size_t ult, size_t uln) | |
| { | |
| throwOnStopped("Progress callback called on cleaned "~ | |
| "up Curl instance"); | |
| return callback(dlt, dln, ult, uln); | |
| }; | |
| set(CurlOption.noprogress, 0); | |
| set(CurlOption.progressdata, cast(void*) &this); | |
| set(CurlOption.progressfunction, cast(void*) &Curl._progressCallback); | |
| } | |
| // Internal C callbacks to register with libcurl | |
| extern (C) private static | |
| size_t _receiveCallback(const char* str, | |
| size_t size, size_t nmemb, void* ptr) | |
| { | |
| auto b = cast(Curl*) ptr; | |
| if (b._onReceive != null) | |
| return b._onReceive(cast(InData)(str[0 .. size*nmemb])); | |
| return size*nmemb; | |
| } | |
| extern (C) private static | |
| size_t _receiveHeaderCallback(const char* str, | |
| size_t size, size_t nmemb, void* ptr) | |
| { | |
| import std.string : chomp; | |
| auto b = cast(Curl*) ptr; | |
| auto s = str[0 .. size*nmemb].chomp(); | |
| if (b._onReceiveHeader != null) | |
| b._onReceiveHeader(s); | |
| return size*nmemb; | |
| } | |
| extern (C) private static | |
| size_t _sendCallback(char *str, size_t size, size_t nmemb, void *ptr) | |
| { | |
| Curl* b = cast(Curl*) ptr; | |
| auto a = cast(void[]) str[0 .. size*nmemb]; | |
| if (b._onSend == null) | |
| return 0; | |
| return b._onSend(a); | |
| } | |
| extern (C) private static | |
| int _seekCallback(void *ptr, curl_off_t offset, int origin) | |
| { | |
| auto b = cast(Curl*) ptr; | |
| if (b._onSeek == null) | |
| return CurlSeek.cantseek; | |
| // origin: CurlSeekPos.set/current/end | |
| // return: CurlSeek.ok/fail/cantseek | |
| return b._onSeek(cast(long) offset, cast(CurlSeekPos) origin); | |
| } | |
| extern (C) private static | |
| int _socketOptionCallback(void *ptr, | |
| curl_socket_t curlfd, curlsocktype purpose) | |
| { | |
| auto b = cast(Curl*) ptr; | |
| if (b._onSocketOption == null) | |
| return 0; | |
| // return: 0 ok, 1 fail | |
| return b._onSocketOption(curlfd, cast(CurlSockType) purpose); | |
| } | |
| extern (C) private static | |
| int _progressCallback(void *ptr, | |
| double dltotal, double dlnow, | |
| double ultotal, double ulnow) | |
| { | |
| auto b = cast(Curl*) ptr; | |
| if (b._onProgress == null) | |
| return 0; | |
| // return: 0 ok, 1 fail | |
| return b._onProgress(cast(size_t) dltotal, cast(size_t) dlnow, | |
| cast(size_t) ultotal, cast(size_t) ulnow); | |
| } | |
| } | |
| // Internal messages send between threads. | |
| // The data is wrapped in this struct in order to ensure that | |
| // other std.concurrency.receive calls does not pick up our messages | |
| // by accident. | |
| private struct CurlMessage(T) | |
| { | |
| public T data; | |
| } | |
| private static CurlMessage!T curlMessage(T)(T data) | |
| { | |
| return CurlMessage!T(data); | |
| } | |
| // Pool of to be used for reusing buffers | |
| private struct Pool(Data) | |
| { | |
| private struct Entry | |
| { | |
| Data data; | |
| Entry* next; | |
| } | |
| private Entry* root; | |
| private Entry* freeList; | |
| @safe @property bool empty() | |
| { | |
| return root == null; | |
| } | |
| @safe nothrow void push(Data d) | |
| { | |
| if (freeList == null) | |
| { | |
| // Allocate new Entry since there is no one | |
| // available in the freeList | |
| freeList = new Entry; | |
| } | |
| freeList.data = d; | |
| Entry* oldroot = root; | |
| root = freeList; | |
| freeList = freeList.next; | |
| root.next = oldroot; | |
| } | |
| @safe Data pop() | |
| { | |
| import std.exception : enforce; | |
| enforce!Exception(root != null, "pop() called on empty pool"); | |
| auto d = root.data; | |
| auto n = root.next; | |
| root.next = freeList; | |
| freeList = root; | |
| root = n; | |
| return d; | |
| } | |
| } | |
| // Lazily-instantiated namespace to avoid importing std.concurrency until needed. | |
| private struct _async() | |
| { | |
| static: | |
| // https://issues.dlang.org/show_bug.cgi?id=15831 | |
| // this should be inside byLineAsync | |
| // Range that reads one chunk at a time asynchronously. | |
| private struct ChunkInputRange | |
| { | |
| import std.concurrency : Tid, send; | |
| private ubyte[] chunk; | |
| mixin WorkerThreadProtocol!(ubyte, chunk); | |
| private Tid workerTid; | |
| private State running; | |
| private this(Tid tid, size_t transmitBuffers, size_t chunkSize) | |
| { | |
| workerTid = tid; | |
| state = State.needUnits; | |
| // Send buffers to other thread for it to use. Since no mechanism is in | |
| // place for moving ownership a cast to shared is done here and a cast | |
| // back to non-shared in the receiving end. | |
| foreach (i ; 0 .. transmitBuffers) | |
| { | |
| ubyte[] arr = new ubyte[](chunkSize); | |
| workerTid.send(cast(immutable(ubyte[]))arr); | |
| } | |
| } | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=15831 | |
| // this should be inside byLineAsync | |
| // Range that reads one line at a time asynchronously. | |
| private static struct LineInputRange(Char) | |
| { | |
| private Char[] line; | |
| mixin WorkerThreadProtocol!(Char, line); | |
| private Tid workerTid; | |
| private State running; | |
| private this(Tid tid, size_t transmitBuffers, size_t bufferSize) | |
| { | |
| import std.concurrency : send; | |
| workerTid = tid; | |
| state = State.needUnits; | |
| // Send buffers to other thread for it to use. Since no mechanism is in | |
| // place for moving ownership a cast to shared is done here and casted | |
| // back to non-shared in the receiving end. | |
| foreach (i ; 0 .. transmitBuffers) | |
| { | |
| auto arr = new Char[](bufferSize); | |
| workerTid.send(cast(immutable(Char[]))arr); | |
| } | |
| } | |
| } | |
| import std.concurrency : Tid; | |
| // Shared function for reading incoming chunks of data and | |
| // sending the to a parent thread | |
| private size_t receiveChunks(ubyte[] data, ref ubyte[] outdata, | |
| Pool!(ubyte[]) freeBuffers, | |
| ref ubyte[] buffer, Tid fromTid, | |
| ref bool aborted) | |
| { | |
| import std.concurrency : receive, send, thisTid; | |
| immutable datalen = data.length; | |
| // Copy data to fill active buffer | |
| while (!data.empty) | |
| { | |
| // Make sure a buffer is present | |
| while ( outdata.empty && freeBuffers.empty) | |
| { | |
| // Active buffer is invalid and there are no | |
| // available buffers in the pool. Wait for buffers | |
| // to return from main thread in order to reuse | |
| // them. | |
| receive((immutable(ubyte)[] buf) | |
| { | |
| buffer = cast(ubyte[]) buf; | |
| outdata = buffer[]; | |
| }, | |
| (bool flag) { aborted = true; } | |
| ); | |
| if (aborted) return cast(size_t) 0; | |
| } | |
| if (outdata.empty) | |
| { | |
| buffer = freeBuffers.pop(); | |
| outdata = buffer[]; | |
| } | |
| // Copy data | |
| auto copyBytes = outdata.length < data.length ? | |
| outdata.length : data.length; | |
| outdata[0 .. copyBytes] = data[0 .. copyBytes]; | |
| outdata = outdata[copyBytes..$]; | |
| data = data[copyBytes..$]; | |
| if (outdata.empty) | |
| fromTid.send(thisTid, curlMessage(cast(immutable(ubyte)[])buffer)); | |
| } | |
| return datalen; | |
| } | |
| // ditto | |
| private void finalizeChunks(ubyte[] outdata, ref ubyte[] buffer, | |
| Tid fromTid) | |
| { | |
| import std.concurrency : send, thisTid; | |
| if (!outdata.empty) | |
| { | |
| // Resize the last buffer | |
| buffer.length = buffer.length - outdata.length; | |
| fromTid.send(thisTid, curlMessage(cast(immutable(ubyte)[])buffer)); | |
| } | |
| } | |
| // Shared function for reading incoming lines of data and sending the to a | |
| // parent thread | |
| private static size_t receiveLines(Terminator, Unit) | |
| (const(ubyte)[] data, ref EncodingScheme encodingScheme, | |
| bool keepTerminator, Terminator terminator, | |
| ref const(ubyte)[] leftOverBytes, ref bool bufferValid, | |
| ref Pool!(Unit[]) freeBuffers, ref Unit[] buffer, | |
| Tid fromTid, ref bool aborted) | |
| { | |
| import std.concurrency : prioritySend, receive, send, thisTid; | |
| import std.exception : enforce; | |
| import std.format : format; | |
| import std.traits : isArray; | |
| immutable datalen = data.length; | |
| // Terminator is specified and buffers should be resized as determined by | |
| // the terminator | |
| // Copy data to active buffer until terminator is found. | |
| // Decode as many lines as possible | |
| while (true) | |
| { | |
| // Make sure a buffer is present | |
| while (!bufferValid && freeBuffers.empty) | |
| { | |
| // Active buffer is invalid and there are no available buffers in | |
| // the pool. Wait for buffers to return from main thread in order to | |
| // reuse them. | |
| receive((immutable(Unit)[] buf) | |
| { | |
| buffer = cast(Unit[]) buf; | |
| buffer.length = 0; | |
| buffer.assumeSafeAppend(); | |
| bufferValid = true; | |
| }, | |
| (bool flag) { aborted = true; } | |
| ); | |
| if (aborted) return cast(size_t) 0; | |
| } | |
| if (!bufferValid) | |
| { | |
| buffer = freeBuffers.pop(); | |
| bufferValid = true; | |
| } | |
| // Try to read a line from left over bytes from last onReceive plus the | |
| // newly received bytes. | |
| try | |
| { | |
| if (decodeLineInto(leftOverBytes, data, buffer, | |
| encodingScheme, terminator)) | |
| { | |
| if (keepTerminator) | |
| { | |
| fromTid.send(thisTid, | |
| curlMessage(cast(immutable(Unit)[])buffer)); | |
| } | |
| else | |
| { | |
| static if (isArray!Terminator) | |
| fromTid.send(thisTid, | |
| curlMessage(cast(immutable(Unit)[]) | |
| buffer[0..$-terminator.length])); | |
| else | |
| fromTid.send(thisTid, | |
| curlMessage(cast(immutable(Unit)[]) | |
| buffer[0..$-1])); | |
| } | |
| bufferValid = false; | |
| } | |
| else | |
| { | |
| // Could not decode an entire line. Save | |
| // bytes left in data for next call to | |
| // onReceive. Can be up to a max of 4 bytes. | |
| enforce!CurlException(data.length <= 4, | |
| format( | |
| "Too many bytes left not decoded %s"~ | |
| " > 4. Maybe the charset specified in"~ | |
| " headers does not match "~ | |
| "the actual content downloaded?", | |
| data.length)); | |
| leftOverBytes ~= data; | |
| break; | |
| } | |
| } | |
| catch (CurlException ex) | |
| { | |
| prioritySend(fromTid, cast(immutable(CurlException))ex); | |
| return cast(size_t) 0; | |
| } | |
| } | |
| return datalen; | |
| } | |
| // ditto | |
| private static | |
| void finalizeLines(Unit)(bool bufferValid, Unit[] buffer, Tid fromTid) | |
| { | |
| import std.concurrency : send, thisTid; | |
| if (bufferValid && buffer.length != 0) | |
| fromTid.send(thisTid, curlMessage(cast(immutable(Unit)[])buffer[0..$])); | |
| } | |
| /* Used by byLineAsync/byChunkAsync to duplicate an existing connection | |
| * that can be used exclusively in a spawned thread. | |
| */ | |
| private void duplicateConnection(Conn, PostData) | |
| (const(char)[] url, Conn conn, PostData postData, Tid tid) | |
| { | |
| import std.concurrency : send; | |
| import std.exception : enforce; | |
| // no move semantic available in std.concurrency ie. must use casting. | |
| auto connDup = conn.dup(); | |
| connDup.url = url; | |
| static if ( is(Conn : HTTP) ) | |
| { | |
| connDup.p.headersOut = null; | |
| connDup.method = conn.method == HTTP.Method.undefined ? | |
| HTTP.Method.get : conn.method; | |
| if (postData !is null) | |
| { | |
| if (connDup.method == HTTP.Method.put) | |
| { | |
| connDup.handle.set(CurlOption.infilesize_large, | |
| postData.length); | |
| } | |
| else | |
| { | |
| // post | |
| connDup.method = HTTP.Method.post; | |
| connDup.handle.set(CurlOption.postfieldsize_large, | |
| postData.length); | |
| } | |
| connDup.handle.set(CurlOption.copypostfields, | |
| cast(void*) postData.ptr); | |
| } | |
| tid.send(cast(ulong) connDup.handle.handle); | |
| tid.send(connDup.method); | |
| } | |
| else | |
| { | |
| enforce!CurlException(postData is null, | |
| "Cannot put ftp data using byLineAsync()"); | |
| tid.send(cast(ulong) connDup.handle.handle); | |
| tid.send(HTTP.Method.undefined); | |
| } | |
| connDup.p.curl.handle = null; // make sure handle is not freed | |
| } | |
| // Spawn a thread for handling the reading of incoming data in the | |
| // background while the delegate is executing. This will optimize | |
| // throughput by allowing simultaneous input (this struct) and | |
| // output (e.g. AsyncHTTPLineOutputRange). | |
| private static void spawn(Conn, Unit, Terminator = void)() | |
| { | |
| import std.concurrency : Tid, prioritySend, receiveOnly, send, thisTid; | |
| import etc.c.curl : CURL, CurlError; | |
| Tid fromTid = receiveOnly!Tid(); | |
| // Get buffer to read into | |
| Pool!(Unit[]) freeBuffers; // Free list of buffer objects | |
| // Number of bytes filled into active buffer | |
| Unit[] buffer; | |
| bool aborted = false; | |
| EncodingScheme encodingScheme; | |
| static if ( !is(Terminator == void)) | |
| { | |
| // Only lines reading will receive a terminator | |
| const terminator = receiveOnly!Terminator(); | |
| const keepTerminator = receiveOnly!bool(); | |
| // max number of bytes to carry over from an onReceive | |
| // callback. This is 4 because it is the max code units to | |
| // decode a code point in the supported encodings. | |
| auto leftOverBytes = new const(ubyte)[4]; | |
| leftOverBytes.length = 0; | |
| auto bufferValid = false; | |
| } | |
| else | |
| { | |
| Unit[] outdata; | |
| } | |
| // no move semantic available in std.concurrency ie. must use casting. | |
| auto connDup = cast(CURL*) receiveOnly!ulong(); | |
| auto client = Conn(); | |
| client.p.curl.handle = connDup; | |
| // receive a method for both ftp and http but just use it for http | |
| auto method = receiveOnly!(HTTP.Method)(); | |
| client.onReceive = (ubyte[] data) | |
| { | |
| // If no terminator is specified the chunk size is fixed. | |
| static if ( is(Terminator == void) ) | |
| return receiveChunks(data, outdata, freeBuffers, buffer, | |
| fromTid, aborted); | |
| else | |
| return receiveLines(data, encodingScheme, | |
| keepTerminator, terminator, leftOverBytes, | |
| bufferValid, freeBuffers, buffer, | |
| fromTid, aborted); | |
| }; | |
| static if ( is(Conn == HTTP) ) | |
| { | |
| client.method = method; | |
| // register dummy header handler | |
| client.onReceiveHeader = (in char[] key, in char[] value) | |
| { | |
| if (key == "content-type") | |
| encodingScheme = EncodingScheme.create(client.p.charset); | |
| }; | |
| } | |
| else | |
| { | |
| encodingScheme = EncodingScheme.create(client.encoding); | |
| } | |
| // Start the request | |
| CurlCode code; | |
| try | |
| { | |
| code = client.perform(No.throwOnError); | |
| } | |
| catch (Exception ex) | |
| { | |
| prioritySend(fromTid, cast(immutable(Exception)) ex); | |
| fromTid.send(thisTid, curlMessage(true)); // signal done | |
| return; | |
| } | |
| if (code != CurlError.ok) | |
| { | |
| if (aborted && (code == CurlError.aborted_by_callback || | |
| code == CurlError.write_error)) | |
| { | |
| fromTid.send(thisTid, curlMessage(true)); // signal done | |
| return; | |
| } | |
| prioritySend(fromTid, cast(immutable(CurlException)) | |
| new CurlException(client.p.curl.errorString(code))); | |
| fromTid.send(thisTid, curlMessage(true)); // signal done | |
| return; | |
| } | |
| // Send remaining data that is not a full chunk size | |
| static if ( is(Terminator == void) ) | |
| finalizeChunks(outdata, buffer, fromTid); | |
| else | |
| finalizeLines(bufferValid, buffer, fromTid); | |
| fromTid.send(thisTid, curlMessage(true)); // signal done | |
| } | |
| } |