diff options
Diffstat (limited to 'libphobos/src/std/net/curl.d')
-rw-r--r-- | libphobos/src/std/net/curl.d | 5109 |
1 files changed, 5109 insertions, 0 deletions
diff --git a/libphobos/src/std/net/curl.d b/libphobos/src/std/net/curl.d new file mode 100644 index 0000000..3465bdf --- /dev/null +++ b/libphobos/src/std/net/curl.d @@ -0,0 +1,5109 @@ +// 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 to 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 http://dlang.org/download.html, download page). + +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. Futhermore it provides <a href="std_range.html">$(D range)</a> +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) provides asynchronous <a +href="std_range.html">$(D ranges)</a> that performs the request in another +thread while handling a line/chunk in the current thread. + +The low level API allows for streaming 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")) returns a range of char[] containing the dlang.org web + page asynchronously.) +) +$(TR $(TDNW $(LREF byChunkAsync)) $(TD $(D +byChunkAsync("dlang.org", 10)) returns a range of ubyte[10] containing the +dlang.org web page asynchronously.) +) +$(LEADINGROW Low level +) +$(TR $(TDNW $(LREF HTTP)) $(TD $(D HTTP) struct for advanced usage)) +$(TR $(TDNW $(LREF FTP)) $(TD $(D FTP) struct for advanced usage)) +$(TR $(TDNW $(LREF SMTP)) $(TD $(D SMTP) struct for advanced 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 should be +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 effected 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 functionally 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; + +import core.thread; +import etc.c.curl; +import std.concurrency; +import std.encoding; +import std.exception; +import std.meta; +import std.range.primitives; +import std.socket : InternetAddress; +import std.traits; +import std.typecons; + +import std.internal.cstring; + +public import etc.c.curl : CurlOption; + +version (unittest) +{ + // Run unit test with the PHOBOS_TEST_ALLOW_NET=1 set in order to + // allow net traffic + import std.range; + import std.stdio; + + import std.socket : Address, INADDR_LOOPBACK, Socket, TcpSocket; + + private struct TestServer + { + string addr() { return _addr; } + + void handle(void function(Socket s) dg) + { + tid.send(dg); + } + + private: + string _addr; + Tid tid; + + static void loop(shared TcpSocket listener) + { + try while (true) + { + void function(Socket) handler = void; + try + handler = receiveOnly!(typeof(handler)); + catch (OwnerTerminated) + return; + handler((cast() listener).accept); + } + catch (Throwable e) + { + import core.stdc.stdlib : exit, EXIT_FAILURE; + stderr.writeln(e); + exit(EXIT_FAILURE); // Bugzilla 7018 + } + } + } + + private TestServer startServer() + { + 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); + } + + private ref TestServer testServer() + { + __gshared TestServer server; + return initOnce!server(startServer()); + } + + 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. + * string 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("d-lang.appspot.com/testUrl2", "/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) 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", "d-lang.appspot.com/testUrl2"); + * ---- + */ +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) 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). If asking + * for $(D 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("d-lang.appspot.com/testUrl2"); + * ---- + * + * Returns: + * A T[] range containing the content of the resource pointed to by the URL. + * + * Throws: + * + * $(D 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 $(D 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). If asking + * for $(D 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("d-lang.appspot.com/testUrl2", ["name1" : "value1", "name2" : "value2"]); + * auto content2 = post("d-lang.appspot.com/testUrl2", [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(url, urlEncode(postDict), conn); +} + +@system unittest +{ + foreach (host; [testServer.addr, "http://" ~ testServer.addr]) + { + testServer.handle((s) { + auto req = s.recvReq!char; + s.send(httpOK(req.bdy)); + }); + auto res = post(host ~ "/path", ["name1" : "value1", "name2" : "value2"]); + assert(res == "name1=value1&name2=value2"); + } +} + +/** 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). If asking + * for $(D 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("d-lang.appspot.com/testUrl2", + * "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("d-lang.appspot.com/testUrl2"); + * ---- + * + * 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; + + 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). + * + * Example: + * ---- + * import std.net.curl; + * auto http = HTTP(); + * options("d-lang.appspot.com/testUrl2", 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). + * + * Example: + * ---- + * import std.net.curl; + * trace("d-lang.appspot.com/testUrl1"); + * ---- + * + * 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). + * + * Example: + * ---- + * import std.net.curl; + * connect("d-lang.appspot.com/testUrl1"); + * ---- + * + * 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 $(D T) specifies the type to return. Possible values + * are $(D char) and $(D ubyte) to return $(D char[]) or $(D ubyte[]). + * + * Example: + * ---- + * auto http = HTTP(); + * http.addRequestHeader("Content-Type", "application/json"); + * auto content = patch("d-lang.appspot.com/testUrl2", `{"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; + + 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] = 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; + + 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); +} + +// Bugzilla 14760 - content length must be reset after post +@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] = 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.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 $(D 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 = $(D 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() + { + enforce!CurlException(currentValid, "Cannot call front() on empty range"); + return current; + } + + void popFront() + { + import std.algorithm.searching : findSplitAfter, findSplit; + + 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 $(D 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) +{ + @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.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 std.datetime.stopwatch : StopWatch; + + 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() + { + 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; + } + } + } +} + +// @@@@BUG 15831@@@@ +// this should be inside byLineAsync +// Range that reads one line at a time asynchronously. +private static struct AsyncLineInputRange(Char) +{ + private Char[] line; + mixin WorkerThreadProtocol!(Char, line); + + private Tid workerTid; + private State running; + + private this(Tid tid, size_t transmitBuffers, size_t bufferSize) + { + 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); + } + } +} + +/** 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 $(D conn) parameter with a + * HTTP/FTP instance that has these properties set. + * + * If $(D postData) is non-_null the method will be set to $(D 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 $(D wait(Duration)) method on + * the $(LREF AsyncLineInputRange). 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 = $(D 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 + { + // 50 is just an arbitrary number for now + setMaxMailboxSize(thisTid, 50, OnCrowding.block); + auto tid = spawn(&_spawnAsync!(Conn, Char, Terminator)); + tid.send(thisTid); + tid.send(terminator); + tid.send(keepTerminator == Yes.keepTerminator); + + _asyncDuplicateConnection(url, conn, postData, tid); + + return AsyncLineInputRange!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"])); + } +} + +// @@@@BUG 15831@@@@ +// this should be inside byLineAsync +// Range that reads one chunk at a time asynchronously. +private static struct AsyncChunkInputRange +{ + 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); + } + } +} + +/** 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 $(D conn) parameter with a + * HTTP/FTP instance that has these properties set. + * + * If $(D postData) is non-_null the method will be set to $(D 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 $(D wait(Duration)) + * method on the $(LREF AsyncChunkInputRange). 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 + { + // 50 is just an arbitrary number for now + setMaxMailboxSize(thisTid, 50, OnCrowding.block); + auto tid = spawn(&_spawnAsync!(Conn, ubyte)); + tid.send(thisTid); + + _asyncDuplicateConnection(url, conn, postData, tid); + + return AsyncChunkInputRange(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]])); + } +} + + +/* Used by byLineAsync/byChunkAsync to duplicate an existing connection + * that can be used exclusively in a spawned thread. + */ +private void _asyncDuplicateConnection(Conn, PostData) + (const(char)[] url, Conn conn, PostData postData, Tid tid) +{ + // 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 +} + +/* + 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() +{ + + /// Value to return from $(D onSend)/$(D 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 = 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) + { + 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 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(); + + // Bugzilla 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 $(D 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 $(D .abortRequest) can be returned in order to abort the + * current request. + * The special value $(D .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; + * 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 ult) + * { + * writeln("Progress: downloaded ", dln, " of ", dl); + * writeln("Progress: uploaded ", uln, " of ", ul); + * }; + * 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 $(D ubyte[]) array using the provided EncodingScheme up to maxChars + Returns: Tuple of ubytes read and the $(D 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) +{ + 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 $(D ubyte[]) array using the provided $(D 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; + + // 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: + * --- + * 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(); + * + * // Put with data senders + * 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(); + * + * // Track progress + * 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: $(_HTTP www.ietf.org/rfc/rfc2616.txt, RFC2616) + * + */ +struct HTTP +{ + mixin Protocol; + + import std.datetime.systime : SysTime; + + /// 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 : startsWith; + import std.conv : to; + import std.regex : regex, match; + import std.uni : toLower; + + // 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(); + + const m = match(header, regex(r"^HTTP/(\d+)\.(\d+) (\d+) (.*)$")); + if (m.empty) + { + // Invalid status line + } + else + { + status.majorVersion = to!ushort(m.captures[1]); + status.minorVersion = to!ushort(m.captures[2]); + status.code = to!ushort(m.captures[3]); + status.reason = m.captures[4].idup; + if (onReceiveStatusLine != null) + onReceiveStatusLine(status); + } + return; + } + + // Normal http header + auto m = match(cast(char[]) header, regex("(.*?): (.*)$")); + + auto fieldName = m.captures[1].toLower().idup; + if (fieldName == "content-type") + { + auto mct = match(cast(char[]) m.captures[2], + regex("charset=([^;]*)", "i")); + if (!mct.empty && mct.captures.length > 1) + charset = mct.captures[1].idup; + } + + if (!m.empty && callback !is null) + callback(fieldName, m.captures[2]); + headersIn[fieldName] = m.captures[2].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; + + /** 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 + $(D 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) + { + /// Value to return from $(D onSend)/$(D 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 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 $(D 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 $(D .abortRequest) can be returned in order to abort the + * current request. + * The special value $(D .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; + * 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 ult) + * { + * writeln("Progress: downloaded ", dln, " of ", dl); + * writeln("Progress: uploaded ", uln, " of ", ul); + * }; + * 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.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 $(D 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 $(D etc.c.curl.CurlError.ok). + * + * Params: + * timing = one of the timings defined in $(REF CurlInfo, etc, c, curl). + * The values are: + * $(D etc.c.curl.CurlInfo.namelookup_time), + * $(D etc.c.curl.CurlInfo.connect_time), + * $(D etc.c.curl.CurlInfo.pretransfer_time), + * $(D etc.c.curl.CurlInfo.starttransfer_time), + * $(D etc.c.curl.CurlInfo.redirect_time), + * $(D etc.c.curl.CurlInfo.appconnect_time), + * $(D 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 $(D 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 = http.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 = $(D 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; + * 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; + * 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 + * $(D const(char)[]) arrays are not valid after the delegate has returned. + * + * Example: + * ---- + * import std.net.curl, std.stdio; + * 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 + $(D 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 std.meta : AliasSeq; + + foreach (c; AliasSeq!("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"); + + // Bugzilla 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; + + 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) + { + /// Value to return from $(D onSend)/$(D 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 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 $(D 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 $(D .abortRequest) can be returned in order to abort the + * current request. + * The special value $(D .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) + { + 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 $(D etc.c.curl.CurlError.ok). + * + * Params: + * timing = one of the timings defined in $(REF CurlInfo, etc, c, curl). + * The values are: + * $(D etc.c.curl.CurlInfo.namelookup_time), + * $(D etc.c.curl.CurlInfo.connect_time), + * $(D etc.c.curl.CurlInfo.pretransfer_time), + * $(D etc.c.curl.CurlInfo.starttransfer_time), + * $(D etc.c.curl.CurlInfo.redirect_time), + * $(D etc.c.curl.CurlInfo.appconnect_time), + * $(D 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 $(D 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 = http.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; + + 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.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) + { + /// Value to return from $(D onSend)/$(D 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 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 $(D 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 $(D .abortRequest) can be returned in order to abort the + * current request. + * The special value $(D .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...) + { + 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; + } +} + +/++ + 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; + +import std.typecons : Flag, Yes, No; +/// Flag to specify whether or not an exception is thrown on error. +alias ThrowOnError = Flag!"throwOnError"; + +private struct CurlAPI +{ + static struct API + { + 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() + { + version (Posix) + { + import core.sys.posix.dlfcn : dlsym, dlopen, dlclose, RTLD_LAZY; + alias loadSym = dlsym; + } + else version (Windows) + { + import core.sys.windows.windows : 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 (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.windows : 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 $(D HTTP)/$(D 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 +{ + 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 $(D CURL_READFUNC_ABORT) or $(D 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() + { + 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() + { + 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) + { + 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) + { + 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. $(D 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) + { + 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 + $(D 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 $(D 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; + * 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 $(D 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 $(D Curl.abortRequest) can be returned in + * order to abort the current request. + * The special value $(D 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 $(D 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; + * 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 uln) + * { + * writeln("Progress: downloaded bytes ", dlnow, " of ", dltotal); + * writeln("Progress: uploaded bytes ", ulnow, " of ", ultotal); + * 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() + { + 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; + } +} + +// Shared function for reading incoming chunks of data and +// sending the to a parent thread +private static size_t _receiveAsyncChunks(ubyte[] data, ref ubyte[] outdata, + Pool!(ubyte[]) freeBuffers, + ref ubyte[] buffer, Tid fromTid, + ref bool aborted) +{ + 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 static void _finalizeAsyncChunks(ubyte[] outdata, ref ubyte[] buffer, + Tid fromTid) +{ + 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 _receiveAsyncLines(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.format : format; + + 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 _finalizeAsyncLines(Unit)(bool bufferValid, Unit[] buffer, Tid fromTid) +{ + if (bufferValid && buffer.length != 0) + fromTid.send(thisTid, curlMessage(cast(immutable(Unit)[])buffer[0..$])); +} + + +// 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 _spawnAsync(Conn, Unit, Terminator = void)() +{ + 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 _receiveAsyncChunks(data, outdata, freeBuffers, buffer, + fromTid, aborted); + else + return _receiveAsyncLines(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) ) + _finalizeAsyncChunks(outdata, buffer, fromTid); + else + _finalizeAsyncLines(bufferValid, buffer, fromTid); + + fromTid.send(thisTid, curlMessage(true)); // signal done +} |