mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 05:00:35 +03:00

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