// 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;
    version (LDC) version (Windows) __gshared string _caBundlePath;

    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");

        /* Windows: Official libcurl builds (bundled with LDC) use a LibreSSL backend
         * (no Windows-native Schannel), which requires setting the path to the bundled
         * curl-ca-bundle.crt file. So check (once) if that file exists, in the same
         * directory as libcurl.dll, and then use it later (in `Curl.initialize()`).
         */
        version (LDC) version (Windows)
        {
            import core.sys.windows.winbase;
            import core.sys.windows.winnt;
            import std.conv : to;
            import std.file : exists;
            import std.path : buildPath, dirName;

            // the following was adopted from std.file.thisExePath
            static string getPathToLibcurl(void* handle)
            {
                wchar[MAX_PATH] buf = void;
                wchar[] buffer = buf[];
                while (true) {
                    const len = GetModuleFileNameW(handle, buffer.ptr, cast(DWORD) buffer.length);
                    enforce!CurlException(len, "GetModuleFileName for libcurl failed");
                    if (len != buffer.length)
                        return buffer[0 .. len].to!string;
                    buffer.length *= 2;
                }
            }

            const caBundlePath = buildPath(getPathToLibcurl(handle).dirName, "curl-ca-bundle.crt");
            if (caBundlePath.exists)
                _caBundlePath = caBundlePath;
        }

        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);

        version (LDC) version (Windows)
        {
            if (CurlAPI._caBundlePath.length)
                set(CurlOption.cainfo, CurlAPI._caBundlePath);
        }
    }

    ///
    @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
    }
}

// LDC: extra https unittest on Windows to test SSL with bundled libcurl.dll
version (LDC) version (Windows)
{
    unittest
    {
        get("https://www.google.com");
    }
}
