#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%require-types
%enable-all-warnings
%new-style
%strict-args

%requires ../../../../../qlib/Util.qm
%requires ../../../../../qlib/Mime.qm
%requires ../../../../../qlib/QUnit.qm
%requires ../../../../../qlib/Logger.qm
%requires ../../../../../qlib/HttpServerUtil.qm
%requires ../../../../../qlib/HttpServer.qm

%exec-class Main

class HttpTestServer {
    public {
        int port;
        string body;

        hash<auto> hdr;

        SSLCertificate remote_cert;
    }

    private {
        Counter end(1);
        string resp;
    }

    constructor(string r) {
        resp = r;
        # start a fake server
        Socket s();
        if (s.bind(0, True))
            throw "BIND-ERROR", strerror();

        if (s.listen())
            throw "LISTEN-ERROR", strerror();

        port = s.getSocketInfo().port;

        background startListener(s);
    }

    done() {
        end.dec();
    }

    private startListener(Socket s) {
        Socket s1 = s.accept();
        hdr = s1.readHTTPHeader();
        if (hdr."content-length")
            body = s1.recv(hdr."content-length");
        else if (hdr."transfer-encoding" == "chunked")
            body = s1.readHTTPChunkedBody().body;
        s1.send(resp);
        end.waitForZero();
        s.close();
        s.shutdown();
    }

    string getBody() {
        return body;
    }

    hash<auto> getHeader() {
        return hdr;
    }
}

class TestStreamRequest inherits AbstractStreamRequest {
    private:internal {
        hash<auto> strhdr;
        data data;

        list<auto> l = ();
    }

    # setup the object and the headers for the response
    constructor(HttpListenerInterface listener, AbstractHttpRequestHandler handler, Socket s, hash<auto> cx,
            hash<auto> hdr, auto body)
            : AbstractStreamRequest(listener, handler, s, cx, hdr, body) {
        strhdr."Content-Type" = hdr."content-type";
        if (hdr."transfer-encoding" == "chunked")
            strhdr."X-Was-Chunked" = "yes";
    }

    recvImpl(hash<auto> v) {
        if (v.data) {
            data += v.data;
        } else if (v.hdr) {
            strhdr += v.hdr;
        }
        l += v;
    }

    private auto sendImpl() {
        if (!data) {
            return {};
        }
        return extract data, 0, 4096;
    }

    hash<HttpHandlerResponseInfo> getResponseHeaderMessageImpl() {
        hash<auto> hdr = strhdr + {
            "X-Got-Chunks": l.size() - 1,
        };
        # test for issue #4109
        if (self.hdr.method == "HEAD") {
            hdr."Transfer-Encoding" = "chunked";
        }
        if (!data) {
            data = (self.hdr.path =~ x/\?(.*)/)[0] ?* "OK";
        }
        hash<HttpHandlerResponseInfo> rv = <HttpHandlerResponseInfo>{
            "code": 200,
            "hdr": hdr,
        };
        if (self.hdr."x-do-chunked-response") {
            rv.hdr."Transfer-Encoding" = "chunked";
        } else {
            rv.body = data;
        }
        return rv;
    }
}

class TestDisconnectStreamRequest inherits AbstractStreamRequest {
    private:internal {
    }

    # setup the object and the headers for the response
    constructor(HttpListenerInterface listener, AbstractHttpRequestHandler handler, Socket s, hash<auto> cx, hash<auto> hdr, auto body)
            : AbstractStreamRequest(listener, handler, s, cx, hdr, body) {
    }

    recvImpl(hash<auto> v) {
    }

    hash<HttpResponseInfo> getResponseHeaderMessageImpl() {
        s.close();
        return <HttpResponseInfo>{};
    }
}

class EchoHandler inherits AbstractHttpRequestHandler {
    public {
        static *SSLCertificate remote_cert;
    }

    private:internal {
    }

    # handle all requests as stream requests
    constructor() : AbstractHttpRequestHandler(NOTHING, True) {
    }

    # return the stream request handler
    AbstractStreamRequest getStreamRequestImpl(HttpListenerInterface listener, Socket s, hash<auto> cx,
            hash<auto> hdr, *data body) {
        # test for #1824
        if (body.typeCode() == NT_STRING && (body.encoding() !~ /iso-8859-1/i) &&
                !regex(hdr._qore_orig_content_type, body.encoding(), RE_Caseless)) {
            throw "ENCODING-ERROR", sprintf("string = %y; Content-Type = %y", body, hdr._qore_orig_content_type);
        }
        remote_cert = s.getRemoteCertificate();
        if (hdr."x-test-disconnect") {
            return new TestDisconnectStreamRequest(listener, self, s, cx, hdr, body);
        }
        return new TestStreamRequest(listener, self, s, cx, hdr, body);
    }
}

class RealTestHttpServer inherits HttpServer {
    private:internal {
        EchoHandler eh();
        int port;
    }

    constructor(*string cert_path, *string key_path, *hash<HttpListenerOptionInfo> listener_opts)
            : HttpServer(RealTestHttpServer::getOptions()) {
        setDefaultHandler("echo-handler", eh);
        if (!listener_opts) {
            listener_opts = <HttpListenerOptionInfo>{};
        }
        if (cert_path) {
            listener_opts += http_get_ssl_objects(cert_path, key_path);
        }
        # bind on a random port
        listener_opts.service = 0;
        port = addListener(listener_opts).port;
    }

    int getPort() {
        return port;
    }

    hash<HttpServerOptionInfo> getOptions() {
        Logger logger("test", LoggerLevel::getLevelInfo());
        if (Main::verbose > 2) {
            logger.addAppender(new StdoutAppender());
        }
        return <HttpServerOptionInfo>{
            "logger": logger,
            "debug": True,
        };
    }
}

class TestOutputStream inherits OutputStream {
    private:internal {
        list l = ();
        string str;
    }

    close() {
    }

    write(binary data) {
        string tstr = binary_to_string(data);
        l += tstr;
        str += tstr;
    }

    string getData() {
        return str;
    }

    list getList() {
        return l;
    }
}

class TestInputStream inherits InputStream {
    private:internal {
        list l;
    }

    constructor(list l) {
        self.l = l;
    }

    int peek() {
        return ord(l[0][0]);
    }

    *binary read(int limit) {
        if (!l)
            return;
        return binary(shift l);
    }
}

public class Main inherits QUnit::Test {
    public {
        static int verbose = 0;

        const Utf8Str = "Hät";
        const Iso88591Str = convert_encoding(Utf8Str, "iso-8859-1");

        const ErrorReply1 = sprintf("HTTP/1.1 200 OK\r\nServer: Test/1.0\r\nContent-Encoding: identity\r\nContent-Type: text/html\r\nDate: %s GMT\r\nContent-Length: 0\r\n\r\n",
                                    gmtime().format("Dy, DD Mon YYYY HH:mm:SS"));
        const ErrorReply2 = sprintf("HTTP/1.1 200 OK\r\nServer: Test/1.0\r\nContent-Encoding: identity\r\nContent-Type: text/html\r\nDate: %s GMT\r\n\r\n",
                                    gmtime().format("Dy, DD Mon YYYY HH:mm:SS"));
        const ChunkedReply1 = sprintf("HTTP/1.1 200 OK\r\nServer: Test/1.0\r\nContent-Encoding: identity\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nDate: %s GMT\r\n\r\n4\r\nTest\r\n5\r\n Mess\r\n4\r\nage.\r\n0\r\n\r\n",
                                      gmtime().format("Dy, DD Mon YYYY HH:mm:SS"));
        const NormalReply1 = sprintf("HTTP/1.1 200 OK\r\nServer: Test/1.0\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nDate: %s GMT\r\n\r\nTest Message.",
                                      gmtime().format("Dy, DD Mon YYYY HH:mm:SS"));

        const ErrorReply3 = sprintf("HTTP/1.1 404 Not Found\r
Server: Qore-Test-Server/1.0\r
X-Powered-By: Qore/%s\r
Connection: Close\r
Content-Type: text/plain;charset=utf8\r
Content-Length: 20\r
\r
404 Not Found: Error",
            VersionString);

        const TestMessage = "Test Message.";
        const TestList = ("Test", " Mess", "age.");

        const TestCert = "-----BEGIN PRIVATE KEY-----
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
WkEP9QjmvU4tYxLvhP6peBr7PhLJ66A=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIFhTCCA22gAwIBAgIUWg3AaxJ1kKrhwH7TTcGh3Z0yPEwwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCQ1oxEzARBgNVBAgMClNvbWUtU3RhdGUxDzANBgNVBAcM
BlByYWd1ZTENMAsGA1UECgwEUW9yZTENMAsGA1UEAwwEUW9yZTAgFw0yMDAxMzEw
NjEyMzVaGA8yMTIwMDEwNzA2MTIzNVowUTELMAkGA1UEBhMCQ1oxEzARBgNVBAgM
ClNvbWUtU3RhdGUxDzANBgNVBAcMBlByYWd1ZTENMAsGA1UECgwEUW9yZTENMAsG
A1UEAwwEUW9yZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOaVbGYM
fxHEaq0WXutG8DveHNsw6mymmdi45d9m0CD6M7b8c+0Kt7uyyMtuTqHV9SdJ2zJO
7mEAVgsmaJveLqn3vKjHw8ev3tcH0+4TdN9Dt5ym0dihkLrcfkdW5eSrdLXdNGY3
o2+A6wdA62yGV7LJtz6f+3tZeMoqAVRLS///byr8koQ1SinmEGZUxaJJLUIbGm8N
SBSTxW2CdtC2GsHoVF1GYBDwxx8x9gPEM8qAnyVlYl/Z0oU4GSLamtqUnlmxXTRS
xdhteYVHUSF/mjNMreSv9sG49PFG0BNgaUi4tAIoyxEf9Ro+aWLZVwNkcOxlh4d8
bVgHCeonLXN16UuvJO8oap4YKOb0yyVa1YzMFU/G8bfWpXc0771FXVwFUNFTyUXc
C+OrN6UsHXgrxNyWiKaxkbUjCMKn2tx6O5AQ/cGEq6OeVOBZojhiWIR9CKHQOCTW
gfKMg2B8Yu+VtjLFUoNwD7VC+rp/xo456CrYylAfewYU5ZPWIk+GYH133UwVZs1L
MZvgs7h0WYYY9ebMpTbLXLkrJypNTYF2aoo6Oc/54LdFhQQA3SRfHJSdeydz4PjZ
HTT4hj7AG7X/CU8uzHHE1a5McdnLWfFY/3wbu5mz86tMbnQY5yXKML+wCAa95hAc
qaFrb5CXAHhGeZl4GEvdRLUZUwU+M+omDSu5AgMBAAGjUzBRMB0GA1UdDgQWBBTS
O+Koh2q4QJgVnFud2cW0n2hxjzAfBgNVHSMEGDAWgBTSO+Koh2q4QJgVnFud2cW0
n2hxjzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQC0n27EM59I
5fa6vuSvSFpD0yV0YLsPD6P6Qr+WnosgZD1E7Vs84OT1p/QmHHMbzFzAlzRN050C
FvUNZNYeGGmj+sPrXhc15FI7GKYY5HrF75YQAPfZ0p4kB6VZQA/Y+5t/7dM7tUUQ
KuIP4KKpSdFrnEH2LQ9xO8LwBRum/2yhOU00GuO/M2wQFmDlWnDE4OphpJCW5zHd
+vDWcvnhFr30IHyBNMxau5ad8vnceHX1WmwQW95zeGQ8sQNxtCjRa/05KnjgnPtj
Dk9FCsB2Iw35Ebfk6gO9K78cKxAYGcWyKaHfN0Okt0uyLZAd88ogAspM2DB0VbGe
DnunO/56PjlwRb2oFVltRGioo1HnLJCtUOAaFc1KOlQ/4xqWlSoQ9a1oUtjRrZjP
H960K/rqBYhDQi/TrdpIRSQheS7zIR+quqcSJPSqxn4r/TEVx/8APm0FSF8p0T56
Ps1yVuAUWSVAJ0JivVlJTnG6aQ1h1xHCDnvW5PSG0bL0sqeSYZ4Gt55LWaqWToo9
qibte1W+dyK5IZEpk3f081N6GegIv+ulzcdcedp4oRCtaOUBziqSlZqXXkoiFyQ4
P1CvlJwL79QntIvjfC5nRMHwMy2wE6Eqt57sFlUnvD25CbbX/TNDSNqXhXQbYaCZ
I45SPdad6fpziP1EtT5rfrC0ZH7cMU8edg==
-----END CERTIFICATE-----";

        const MyOpts = {
            "proxy": "P,proxy=s",
        } + Opts;

        const OptionColumn = 22;
    }

    constructor() : Test("HttpClientTest", "1.0", \ARGV, MyOpts) {
        if (m_options.verbose) {
            verbose = m_options.verbose;
        }
        addTestCase("timeouts", \timeoutTest());
        addTestCase("pre-encoded-urls", \preEncodedUrlsTest());
        addTestCase("poll proxy test", \pollProxyTest());
        addTestCase("poll test", \pollTest());
        addTestCase("chunked HEAD test", \chunkedHeadTest());
        addTestCase("assume encoding", \assumedEncoding());
        addTestCase("chunked disconnect test", \chunkedDisconnectTest());
        addTestCase("client certs", \clientCertTest());
        addTestCase("issue 3472", \issue3472());
        # disabled test as the external dependency is currently not working (2019-03-20)
        #addTestCase("issue 3053", \issue3053());
        addTestCase("HttpClient Test", \testClient());
        addTestCase("SSL read test", \sslReadTest());
        addTestCase("issue 2610", \issue2610());
        set_return_value(main());
    }

   private usageIntern() {
        TestReporter::usageIntern(OptionColumn);
        printOption("-P,--proxy=ARG", "the proxy to use for proxy tests", OptionColumn);
    }

    timeoutTest() {
        HTTPClient hc({"url": "http://localhost"});
        assertEq(300000, hc.getTimeout());
        assertEq(60000, hc.getConnectTimeout());
    }

    preEncodedUrlsTest() {
        RealTestHttpServer serv();
        on_exit delete serv;

        HTTPClient hc({
            "url": "http://localhost:" + serv.getPort(),
            "pre_encoded_urls": True,
        });
        hc.connect();

        assertTrue(hc.getPreEncodedUrls());

        assertEq("hello[]", hc.send(NOTHING, "PUT", "?hello%5B%5D").body);

        assertThrows("URL-ENCODING-ERROR", "\\?hello\\[\\]", \hc.send(), (NOTHING, "PUT", "?hello[]"));

        assertTrue(hc.setPreEncodedUrls());
        assertTrue(hc.setPreEncodedUrls(False));
        assertFalse(hc.getPreEncodedUrls());

        assertEq("hello[]", hc.send(NOTHING, "PUT", "?hello[]").body);
        assertEq("hello%5B%5D", hc.send(NOTHING, "PUT", "?hello%5B%5D").body);
    }

    pollProxyTest() {
        #m_options.proxy = "http://198.59.191.234:8080"; #"http://75.119.130.35:3128";
        if (!m_options.proxy) {
            testSkip("no proxy set");
        }

        {
            HTTPClient hc({
                "url": "http://hq.qoretechnologies.com",
                "proxy": m_options.proxy,
            });

            SocketPollOperation spop = hc.startPollConnect();
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());
            assertNothing(spop.getOutput());

            hc.disconnect();
            spop = hc.startPollSendRecv("GET", "/");
            assertEq("received", spop.getGoal());
            assertEq("connecting", spop.getState());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            hash<auto> h = spop.getOutput();
            assertEq(200, h."code");

            spop = hc.startPollSendRecv("GET", "/");
            assertEq("received", spop.getGoal());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
        }

        {
            HTTPClient hc({
                "url": "https://hq.qoretechnologies.com",
                "proxy": m_options.proxy,
            });

            SocketPollOperation spop = hc.startPollConnect();
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());
            hash<auto> h = spop.getOutput();
            assertEq(200, h."code");

            hc.disconnect();
            spop = hc.startPollSendRecv("GET", "/");
            assertEq("received", spop.getGoal());
            assertEq("connecting", spop.getState());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");

            spop = hc.startPollSendRecv("GET", "/");
            assertEq("received", spop.getGoal());

            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
        }
    }

    pollTest() {
        {
            RealTestHttpServer serv();
            on_exit delete serv;

            {
                Socket tmp();
                if (tmp.bind("localhost:0", True)) {
                    throw "SOCKET-BIND-ERROR", strerror();
                }
                tmp.listen();
                int port = tmp.getSocketInfo().port;
                tmp.close();
                tmp.shutdown();

                HTTPClient hc({
                    "url": "https://localhost:" + port,
                });

                SocketPollOperation spop = hc.startPollConnect();
                assertEq("connect", spop.getGoal());
                assertEq("connecting", spop.getState());
                assertThrows("SOCKET-CONNECT-ERROR", \spop.continuePoll());
            }

            HTTPClient hc({
                "url": "http://localhost:" + serv.getPort(),
            });

            SocketPollOperation spop = hc.startPollConnect();
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());
            assertNothing(spop.getOutput());

            hc.disconnect();

            string msg = get_random_string(100);
            assertEq(msg, hc.send(msg, "PUT").body);

            spop = hc.startPollSendRecv("PUT", "/", msg);
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            hash<auto> h = spop.getOutput();
            on_error printf("h: %N\n", h);
            assertEq(200, h."code");

            binary bigmsg = get_random_bytes(10 * 1024 * 1024);
            assertEq(bigmsg, binary(hc.send(bigmsg, "PUT", "/", {"Content-Type": MimeTypeOctetStream}).body));

            spop = hc.startPollSendRecv("PUT", "/", bigmsg, {"Content-Type": MimeTypeOctetStream});
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
            assertEq(bigmsg.size(), h."response-body".size());
            assertEq(bigmsg, h."response-body");

            spop = hc.startPollSendRecv("PUT", "/", bigmsg, {
                "Content-Type": MimeTypeOctetStream,
                "X-Do-Chunked-Response": "True",
            });
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("send/recv info: %y (state: %y)\n", info, spop.getState());
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
            assertEq("chunked", h.info."response-headers"."transfer-encoding");
            assertEq(bigmsg.size(), h."response-body".size());
            assertEq(bigmsg, h."response-body");
        }
        {
            TempFile cert("cert.pem", TestCert);

            RealTestHttpServer serv(cert.getPath(), cert.getPath());
            on_exit delete serv;

            HTTPClient hc({
                "url": "https://localhost:" + serv.getPort(),
            });

            SocketPollOperation spop = hc.startPollConnect();
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());
            assertNothing(spop.getOutput());

            hc.disconnect();

            string msg = get_random_string(100);
            assertEq(msg, hc.send(msg, "PUT").body);

            spop = hc.startPollSendRecv("PUT", "/", msg);
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            hash<auto> h = spop.getOutput();
            on_error printf("h: %N\n", h);
            assertEq(200, h."code");
            assertEq(msg, h."response-body".toString());

            binary bigmsg = get_random_bytes(10 * 1024 * 1024);
            assertEq(bigmsg, binary(hc.send(bigmsg, "PUT", "/", {"Content-Type": MimeTypeOctetStream}).body));

            spop = hc.startPollSendRecv("PUT", "/", bigmsg, {"Content-Type": MimeTypeOctetStream});
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
            assertEq(bigmsg.size(), h."response-body".size());

            spop = hc.startPollSendRecv("PUT", "/", bigmsg, {
                "Content-Type": MimeTypeOctetStream,
                "X-Do-Chunked-Response": "True",
            });
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("send/recv info: %y (state: %y)\n", info, spop.getState());
                Socket::poll((info,), 1s);
            }
            assertEq("received", spop.getState());
            assertTrue(spop.goalReached());
            h = spop.getOutput();
            assertEq(200, h."code");
            assertEq("chunked", h.info."response-headers"."transfer-encoding");
            assertEq(bigmsg.size(), h."response-body".size());
            assertEq(bigmsg, h."response-body");

            spop = hc.startPollSendRecv("PUT", "/", msg);
            assertEq("received", spop.getGoal());
            assertEq("sending", spop.getState());
            hc.disconnect();
            assertThrows("SOCKET-NOT-OPEN", \spop.continuePoll());
            delete spop;

            spop = hc.startPollSendRecv("PUT", "/", msg);
            assertEq("received", spop.getGoal());
            assertEq("connecting", spop.getState());
            delete hc;
            assertThrows("OBJECT-ALREADY-DELETED", \spop.continuePoll());
        }
    }

    assumedEncoding() {
        {
            HTTPClient hc({
                "url": "http://example.com",
                "assume_encoding": "utf-8",
            });
            assertEq("utf-8", hc.getAssumedEncoding());
        }
        {
            HTTPClient hc({
                "url": "http://example.com",
            });
            assertEq("ISO-8859-1", hc.getAssumedEncoding());
            hc.setAssumedEncoding("UTF-8");
            assertEq("UTF-8", hc.getAssumedEncoding());
        }
    }

    chunkedHeadTest() {
        TempFile cert("cert.pem", TestCert);

        RealTestHttpServer serv(cert.getPath(), cert.getPath());
        on_exit delete serv;

        HTTPClient hc({
            "url": "https://localhost:" + serv.getPort(),
        });
        hash<auto> hdr = {
            "Connection": "Keep-Alive",
        };

        hash<auto> resp = hc.send(NOTHING, "HEAD", "/", hdr);
        assertEq(200, resp.status_code);
        assertEq("chunked", resp."transfer-encoding");
        hc.disconnect();
    }

    chunkedDisconnectTest() {
        TempFile cert("cert.pem", TestCert);

        RealTestHttpServer serv(cert.getPath(), cert.getPath());
        on_exit delete serv;

        HTTPClient hc({
            "url": "https://localhost:" + serv.getPort(),
        });
        hash<auto> hdr = {
            "X-Test-Disconnect": "true",
            "Connection": "Keep-Alive",
        };
        # returns nothing - no chunked data sent
        code send_callback = auto sub () {};
        assertThrows("SOCKET-CLOSED", \hc.sendWithSendCallback(), (send_callback, "PUT", NOTHING, hdr, 5m));
        assertFalse(hc.isConnected());
    }

    clientCertTest() {
        TempFile cert("cert.pem", TestCert);
        string cert_data = ReadOnlyFile::readTextFile(cert.getPath());

        SSLCertificate x509_local(cert_data);
        binary sig_local = x509_local.getSignature();

        {
            Queue q();
            Counter done(1);
            on_exit done.dec();
            background sslReadClientCertTest(q, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, True, done);
            # wait for server to be listening and get port
            int port = q.get();

            HTTPClient hc({
                "url": "https://localhost:" + port,
                "ssl_cert_path": cert.getPath(),
                "ssl_key_path": cert.getPath(),
            });
            hc.send(NOTHING, "GET");
            SSLCertificate x509_remote = q.get();
            binary sig_remote = x509_remote.getSignature();
            #printf("%s <=> %s: %y\n", sig_local.toHex(), sig_remote.toHex(), sig_local <=> sig_remote);
            assertEq(sig_local, sig_remote);
        }
        {
            Queue q();
            Counter done(1);
            on_exit done.dec();
            background sslReadClientCertTest(q, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, False, done);

            # wait for server to be listening and get port
            int port = q.get();

            HTTPClient hc({
                "url": "https://localhost:" + port,
                "ssl_cert_path": cert.getPath(),
                "ssl_key_path": cert.getPath(),
            });
            assertThrows("SOCKET-SSL-ERROR", \hc.send(), (NOTHING, "GET"));
            assertEq("SOCKET-SSL-ERROR", q.get().err);
        }
        {
            Queue q();
            Counter done(1);
            on_exit done.dec();
            background sslReadClientCertTest(q, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, False, done);

            # wait for server to be listening and get port
            int port = q.get();

            HTTPClient hc({
                "url": "http://localhost:" + port,
                "ssl_cert_path": cert.getPath(),
                "ssl_key_path": cert.getPath(),
            });
            assertThrows("SOCKET-CLOSED", \hc.send(), (NOTHING, "GET"));
        }
        {
            RealTestHttpServer serv(cert.getPath(), cert.getPath(), <HttpListenerOptionInfo>{"get_remote_certs": True});
            on_exit delete serv;

            HTTPClient hc({
                "url": "https://localhost:" + serv.getPort(),
                "ssl_cert_path": cert.getPath(),
                "ssl_key_path": cert.getPath(),
            });
            hc.send("", "PUT", NOTHING, {"Connection": "close"});
            binary sig_remote = EchoHandler::remote_cert.getSignature();
            #printf("%s <=> %s: %y\n", sig_local.toHex(), sig_remote.toHex(), sig_local <=> sig_remote);
            assertEq(sig_local, sig_remote);

            hash<auto> info = serv.getListenerInfo("listener-0");
            assertEq(True, info.get_remote_certs);
            assertEq(True, info.ssl_accept_all_certs);
            assertEq(("SSL_VERIFY_PEER",), info.ssl_verify_flags);
            assertFalse(hc.getAcceptAllCertificates());
            hc.setSslVerifyMode(SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
            assertThrows("SOCKET-SSL-ERROR", "self.signed cert", \hc.send(), ("", "PUT", NOTHING, {"Connection": "close"}));
        }

        /* test disabled by default due to dependency on external resource ...
        {
            HTTPClient hc({
                "url": "https://qoretechnologies.com",
            });
            assertFalse(hc.getAcceptAllCertificates());
            hc.setSslVerifyMode(SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT);
            assertEq(Type::Hash, hc.send("", "GET", NOTHING, {"Connection": "close"}).type());
        }
        */
    }

    issue3472() {
        {
            HttpTestServer serv(ErrorReply3);
            on_exit serv.done();
            HTTPClient hc({
                "url": "http://localhost:" + serv.port,
                "error_passthru": True,
                "headers": {"X-Test": 1},
            });
            hash<auto> info;
            hash<auto> resp = hc.send(NOTHING, "GET", "/", NOTHING, False, \info);
            assertEq(404, resp.status_code);
            assertEq("Close", resp.connection);
            assertEq("Close", info."response-headers-raw".Connection);
            assertEq("Close", info."response-headers".connection);
            assertEq("1", info.headers."X-Test");
            assertEq("1", hc.getDefaultHeaders()."X-Test");
        }

        {
            HttpTestServer serv(ErrorReply3);
            on_exit serv.done();
            HTTPClient hc({
                "url": "http://localhost:" + serv.port,
                "error_passthru": True,
                "headers": {"X-Test": 1},
            });
            hc.addDefaultHeaders({"X-Test-2": "x"});
            hash<auto> info;
            hash<auto> resp = hc.send(NOTHING, "GET", "/", NOTHING, False, \info);
            assertEq(404, resp.status_code);
            assertEq("Close", resp.connection);
            assertEq("Close", info."response-headers-raw".Connection);
            assertEq("Close", info."response-headers".connection);
            assertEq("1", hc.getDefaultHeaders()."X-Test");
            assertEq("x", hc.getDefaultHeaders()."X-Test-2");
        }

        {
            assertThrows("HTTP-CLIENT-OPTION-ERROR", "\"headers\"",
                sub () { new HTTPClient({"url": "http://localhost:1111", "headers": "x"}); });
        }

        {
            HttpTestServer serv(ErrorReply3);
            on_exit serv.done();
            HTTPClient hc({"url": "http://localhost:" + serv.port, "error_passthru": True});
            hc.setErrorPassthru(False);
            assertThrows("HTTP-CLIENT-RECEIVE-ERROR", "404", \hc.send(), (NOTHING, "GET", "/"));
        }
    }

    issue3053() {
        HTTPClient httpclient({"url":"https://api.exchangeratesapi.io/latest"});
        hash<auto> h = httpclient.send("", "GET", "https://api.exchangeratesapi.io/latest");
        assertEq(200, h.status_code);
    }

    testClient() {
        {
            HttpTestServer serv(ErrorReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));
            hash info;
            *string str = hc.get("/something", NOTHING, \info);
            assertEq(serv.port, (info.headers.Host =~ x/:([0-9]+)/)[0].toInt());
            assertEq(NOTHING, str);
            hc.disconnect();
        }
        {
            HttpTestServer serv(ErrorReply2);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));
            *string str = hc.get("/something");
            assertEq(NOTHING, str);
            hc.disconnect();
        }
        {
            HttpTestServer serv(ChunkedReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));
            *string str = hc.get("/something");
            assertEq(TestMessage, str);
        }
        {
            HttpTestServer serv(ChunkedReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));
            *string str = hc.get("/something", ("Test-Header": "test1", "test-HEADER": "test2", "teST-HeAdEr": ("test3","test4")));
            assertEq(TestMessage, str);
            assertEq("test3,test4", serv.getHeader()."test-header");
        }

        {
            # test receiving a chunked HTTP message with an output stream
            HttpTestServer serv(ChunkedReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));

            list l = ();
            code rcb = sub (hash h) { l += h; };
            TestOutputStream strstr();
            hc.send(strstr, NOTHING, "GET", "/something", NOTHING, 5s, False, NOTHING, rcb);
            assertEq(TestMessage, strstr.getData());
            assertEq(2, l.size());
            list ml = strstr.getList();
            assertEq(3, ml.size());
            assertEq(TestList, ml);

            hash h = l[0];
            assertEq("1.1", h.hdr.http_version);
            assertEq(200, h.hdr.status_code);
            assertEq("OK", h.hdr.status_message);
            assertEq("Test/1.0", h.hdr.server);
            assertEq("text/plain", h.hdr."content-type");
            assertEq("chunked", h.hdr."transfer-encoding");
            assertEq("text/plain", h.hdr._qore_orig_content_type);
            assertEq(hc, h.obj);
            assertEq(False, h.send_aborted);

            h = l[1];
            assertEq(NOTHING, h.hdr);
            assertEq(hc, h.obj);
            assertEq(False, h.send_aborted);
        }

        {
            # test receiving a normal HTTP message with an output stream
            HttpTestServer serv(NormalReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));

            list l = ();
            code rcb = sub (hash h) { l += h; };
            TestOutputStream strstr();
            hc.send(strstr, NOTHING, "GET", "/something", NOTHING, 5s, False, NOTHING, rcb);
            assertEq(TestMessage, strstr.getData());
            assertEq(1, l.size());
            list ml = strstr.getList();
            assertEq(TestMessage, ml[0]);
            assertEq(1, ml.size());

            hash h = l[0];
            assertEq("1.1", h.hdr.http_version);
            assertEq(200, h.hdr.status_code);
            assertEq("OK", h.hdr.status_message);
            assertEq("Test/1.0", h.hdr.server);
            assertEq("text/plain", h.hdr."content-type");
            assertEq("text/plain", h.hdr._qore_orig_content_type);
            assertEq(hc, h.obj);
            assertEq(False, h.send_aborted);
        }

        {
            HTTPClient hc(("url": "http://localhost"));
            hash info;
            try { hc.get("", NOTHING, \info); } catch () {}
            assertEq(Type::String, info.headers.Host.type());
            assertEq(True, info.headers.Host !~ /:/);
            hc.setURL("http://localhost:80");
            try { hc.get("", NOTHING, \info); } catch () {}
            assertEq(Type::String, info.headers.Host.type());
            assertEq(True, info.headers.Host !~ /:/);
            hc.setURL("https://localhost");
            try { hc.get("", NOTHING, \info); } catch () {}
            assertEq(Type::String, info.headers.Host.type());
            assertEq(True, info.headers.Host !~ /:/);
            hc.setURL("https://localhost:443");
            try { hc.get("", NOTHING, \info); } catch () {}
            assertEq(Type::String, info.headers.Host.type());
            assertEq(True, info.headers.Host !~ /:/);
        }

        {
            # issue 1728: check URL handling
            string bad_url = "http://localhost:1234\"";
            assertThrows("HTTP-CLIENT-URL-ERROR", sub () { HTTPClient hc(("url": bad_url)); });
        }

        {
            # issue 1813: check message body conversion
            HttpTestServer serv(ErrorReply1);
            on_exit serv.done();
            HTTPClient hc(("url": "http://localhost:" + serv.port));
            hc.setEncoding("iso-8859-1");
            hc.post("/something", Utf8Str, ("Content-Type": MimeTypeText));
            assertEq(Iso88591Str, serv.getBody());
        }

        {
            RealTestHttpServer hs();
            on_exit
                delete hs;

            HTTPClient hc(("url": "http://localhost:" + hs.getPort()));

            # send monolithic message and get back single message in a stream
            list l = ();
            code rcb = sub (hash h) { l += h; };
            TestOutputStream strstr();
            hc.send(strstr, TestMessage, "POST", "/something", {"Content-Type": MimeTypeText}, 5s, False, NOTHING, rcb);
            assertEq(TestMessage, strstr.getData());
            assertEq(1, l.size());
            list ml = strstr.getList();
            assertEq(1, ml.size());
            assertEq((TestMessage,), ml);

            hash h = l[0];
            assertEq("1.1", h.hdr.http_version);
            assertEq(200, h.hdr.status_code);
            assertEq("OK", h.hdr.status_message);
            assertEq(NOTHING, h.hdr."x-was-chunked");
            assertEq("1", h.hdr."x-got-chunks");
            assertEq("text/plain", h.hdr."content-type");
            assertEq("text/plain;charset=utf8", h.hdr._qore_orig_content_type);
            assertEq(hc, h.obj);
            assertEq(False, h.send_aborted);

            # send a streamed/chunked message and get back a single message in a stream
            TestInputStream is(TestList);
            strstr = new TestOutputStream();
            l = ();
            code tcb = *hash sub () {
                return ("X-Content-Done": "true");
            };
            hc.sendChunked(strstr, is, "POST", NOTHING, "/something",
                {
                    "Content-Type": MimeTypeText,
                    "Connection": "close"
                }, 5s, False, NOTHING, rcb, tcb);
            assertEq(TestMessage, strstr.getData());
            assertEq(1, l.size());
            ml = strstr.getList();
            assertEq(1, ml.size());
            assertEq((TestMessage,), ml);

            h = l[0];
            assertEq("1.1", h.hdr.http_version);
            assertEq(200, h.hdr.status_code);
            assertEq("OK", h.hdr.status_message);
            assertEq("yes", h.hdr."x-was-chunked");
            assertEq("3", h.hdr."x-got-chunks");
            assertEq("true", h.hdr."x-content-done");
            assertEq("text/plain", h.hdr."content-type");
            assertEq("text/plain;charset=utf8", h.hdr._qore_orig_content_type);
            assertEq(hc, h.obj);
            assertEq(False, h.send_aborted);

            hash<auto> info = hs.getListenerInfo("listener-0");
            assertEq(False, info.get_remote_certs);
            assertEq(True, info.ssl_accept_all_certs);
            assertEq(("SSL_VERIFY_NONE",), info.ssl_verify_flags);
        }

        {
            HTTPClient hc(("url": "http://localhost"));
            string rpath = get_random_string();
            hc.setConnectionPath(rpath);
            assertEq(rpath, hc.getConnectionPath());

            hc.setConnectionPath();
            assertNothing(hc.getConnectionPath());
        }

        {
            # test receiving a normal HTTP message with ISO-8859-1 encoding when the client specifies UTF-8
            HttpTestServer serv(NormalReply1);
            on_exit serv.done();
            HTTPClient hc({"url": "http://localhost:" + serv.port, "encoding": "utf-8"});
            assertEq("UTF-8", hc.getEncoding());
            hc.post("/something", Utf8Str, {"Content-Type": MimeTypeText + ";charset=utf-8"});
            assertEq(Iso88591Str, serv.getBody());
            assertEq("UTF-8", hc.getEncoding());
        }
    }

    sslReadTest() {
        Queue q();
        background sslReadError(q);

        # wait for server to be listening and get port
        int port = q.get();

        HTTPClient hc(("url": "https://localhost:" + port));
        try {
            hc.get("/");
            fail();
        } catch (hash<ExceptionInfo> ex) {
            if ((ex.err == "SOCKET-CLOSED") ||
                (ex.err == "SOCKET-SSL-ERROR" && ex.desc =~ /reset by peer/)) {
                assertTrue(True);
            } else {
                fail();
            }
        }

        # test is not reliable
        # issue 1488: currently only reproducible with the following URL
        #hc.setURL("https://group.three.com/qcbin/rest/is-authenticated");
        #hc.setTimeout(15s);
        #assertThrows("SOCKET-SSL-ERROR", "Connection reset", \hc.get(), ("", ("Host": "group.three.com:443")));
    }

    issue2610() {
        HTTPClient hc();
        # issue 2610: this causes a crash
        hc.setDefaultPath();
        assertNothing(hc.getDefaultPath());
    }

    sslReadError(Queue q) {
        Socket s();
        s.bind(0);
        s.listen();
        s.setCertificate(TestCert);
        s.setPrivateKey(TestCert);
        q.push(s.getSocketInfo().port);
        Socket ns = s.acceptSSL(15s);
        ns.bind(0);
    }

    sslReadClientCertTest(Queue q, int verify_mode, bool accept_all, Counter done) {
        Socket s();
        s.setSslVerifyMode(verify_mode);
        if (accept_all) {
            s.acceptAllCertificates();
        }
        s.captureRemoteCertificates();
        s.bind(0);
        s.listen();
        s.setCertificate(TestCert);
        s.setPrivateKey(TestCert);
        q.push(s.getSocketInfo().port);
        try {
            Socket ns = s.acceptSSL(15s);
            q.push(ns.getRemoteCertificate());
            ns.send(NormalReply1);
            # wait until reply has been received to delete the socket
            done.waitForZero();
        } catch (hash<ExceptionInfo> ex) {
            q.push(ex);
        }
    }
}

class TempFile {
    private {
        string path;
    }

    constructor(string name, data content) {
        path = sprintf("%s%s%s", tmp_location(), DirSep, name);
        File f();
        f.open2(path, O_CREAT | O_WRONLY | O_TRUNC);
        f.write(content);
    }

    destructor() {
        unlink(path);
    }

    string getPath() {
        return path;
    }
}
