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

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

%requires ../../../../../qlib/Util.qm
%requires ../../../../../qlib/QUnit.qm

%exec-class SocketTest

const i1 = 10;
const i2 = 5121;
const i4 = 2393921;
const i8 = 12309309203932;

const http_headers = (
    "Accept"       : "text",
    "Content-Type" : "text",
    "User-Agent"   : "Qore HTTP Test Agent",
    "Connection"   : "Keep-Alive",
);

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

class SocketTest inherits QUnit::Test {
    private {
        auto o;
        auto fam;

        const XString = "This is a binary string";
        const XBinary = Qore::binary(XString);

        Counter counter(1);

        Queue queue();
        Counter stopc();

        int server_port;
        int client_port;

        string client_host;

        const MyOpts = {
            "server"     : "S,server=s",
            "servonly"   : "O,server-only",
            "ipv4"       : "4,ipv4",
            "ipv6"       : "6,ipv6",
            "ssl"        : "s,ssl",
            "key"        : "k,private-key=s",
            "cert"       : "c,cert=s",
            "pass"       : "p,private-key-password=s",
            "clientkey"  : "K,client-private-key=s",
            "clientcert" : "C,client-cert=s",
            "events"     : "e,show-events",
        } + Opts;
    }

    constructor() : QUnit::Test("Socket test", "1.0", \ARGV, MyOpts) {
        process_command_line();

        addTestCase("poll test", \pollTest());
        addTestCase("tls13 test", \tls13Test());
        addTestCase("connection ID", \connectionIdTest());
        addTestCase("Client/Server Socket tests", \clientServerSocketTest());
        addTestCase("Unconnected Socket tests", \unconnectedSocketTest());
        addTestCase("Random Port tests", \randomPortSocketTest());
        addTestCase("SSL read test", \sslReadTest());
        addTestCase("SSL write disconnect test", \sslWriteDisconnectTest());
        set_return_value(main());
    }

    private doServ(Queue q) {
        Socket s0();
        # bind on a random free port
        s0.bind(0);
        s0.listen();
        q.push(s0.getSocketInfo().port);
        s0.setCertificate(TestCert);
        s0.setPrivateKey(TestCert);
        s0.acceptSSL();
    }

    pollTest() {
        Socket s0();
        Socket s1();

        list<hash<SocketPollInfo>> poll_list = (
            <SocketPollInfo>{
                "events": SOCK_POLLIN,
                "socket": s0,
            },
            <SocketPollInfo>{
                "events": SOCK_POLLOUT,
                "socket": s1,
            },
        );

        list<hash<SocketPollInfo>> new_list;

        {
            if (s0.bind("127.0.0.1:0", True)) {
                throw "SOCKET-BIND-ERROR", strerror();
            }
            s0.listen();
            int port = s0.getSocketInfo().port;

            Socket s2();

            {
                SocketPollOperation spop = s2.startPollConnect("-1");
                assertThrows("SOCKET-NON-BLOCK-ERROR", \s2.startPollConnect(), "x");
                assertThrows("SOCKET-NON-BLOCK-ERROR", \s2.connect(), "x");
                assertThrows("SOCKET-NON-BLOCK-ERROR", \s2.startPollSend(), "x");
                assertThrows("SOCKET-NON-BLOCK-ERROR", \s2.startPollRecvString(), 1234);
                assertThrows("SOCKET-NON-BLOCK-ERROR", \s2.startPollRecvBinary(), 1234);
                assertEq("connecting", spop.getState());
                assertThrows("SOCKET-CONNECT-ERROR", \spop.continuePoll());
            }
            {
                SocketPollOperation spop = s2.startPollConnect("65536");
                assertEq("connecting", spop.getState());
                assertThrows("SOCKET-CONNECT-ERROR", \spop.continuePoll());
            }
            assertThrows("SOCKET-NOT-OPEN", \s2.startPollSend(), <abcd>);
            assertThrows("SOCKET-NOT-OPEN", \s2.startPollRecvString(), 1234);
            assertThrows("SOCKET-NOT-OPEN", \s2.startPollRecvBinary(), 1234);

            string target = "127.0.0.1:" + port;
            SocketPollOperation spop = s2.startPollConnect(target);
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("info: %y\n", info);
                new_list = Socket::poll((info,), 100ms);
            }
            assertEq("connected", spop.getState());
            assertTrue(spop.goalReached());

            assertTrue(s2.isOpen());
            assertTrue(s0.isOpen());
            assertTrue(s0.isDataAvailable());

            Socket s3 = s0.accept(1ms);
            string test_msg = get_random_string();
            s3.send(test_msg);

            string msg = s2.recv(-1);
            assertEq(test_msg, msg);

            spop = s3.startPollSend(test_msg);
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("send info: %y\n", info);
                new_list = Socket::poll((info,), 1s);
            }

            msg = s2.recv(test_msg.size());
            assertEq(test_msg, msg);

            spop = s3.startPollSend(test_msg);
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("recv info: %y\n", info);
                new_list = Socket::poll((info,), 1s);
            }

            spop = s2.startPollRecvString(test_msg.size());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("recv info: %y\n", info);
                new_list = Socket::poll((info,), 1s);
            }
            assertEq(test_msg, spop.getOutput());

            s3.send(test_msg);
            spop = s2.startPollRecvBinary(test_msg.size());
            while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                #printf("recv info: %y\n", info);
                new_list = Socket::poll((info,), 1s);
            }
            assertEq(binary(test_msg), spop.getOutput());

            binary bigmsg = get_random_bytes(10 * 1024 * 1024);
            {
                SocketPollOperation spop_send = s3.startPollSend(bigmsg);
                SocketPollOperation spop_recv = s2.startPollRecvBinary(bigmsg.size());
                while (True) {
                    poll_list = ();
                    *hash<SocketPollInfo> info_send = spop_send.continuePoll();
                    if (info_send) {
                        #printf("send info: %y\n", info_send);
                        poll_list += info_send;
                    }
                    *hash<SocketPollInfo> info_recv = spop_recv.continuePoll();
                    if (info_recv) {
                        #printf("recv info: %y\n", info_recv);
                        poll_list += info_recv;
                    }
                    if (!poll_list) {
                        break;
                    }
                    new_list = Socket::poll(poll_list, 1s);
                }
                assertEq(bigmsg, spop_recv.getOutput());
            }

            Socket s4();
            if (s4.bind("127.0.0.1:0", True)) {
                throw "SOCKET-BIND-ERROR", strerror();
            }
            s4.listen();
            int port2 = s4.getSocketInfo().port;
            s4.close();
            s4.shutdown();
            target = "127.0.0.1:" + port2;
            spop = s2.startPollConnect(target);
            assertEq("connect", spop.getGoal());
            assertEq("connecting", spop.getState());

            assertThrows("SOCKET-CONNECT-ERROR", \spop.continuePoll());

            spop = s2.startPollConnectSsl(target);
            assertEq("connect-ssl", spop.getGoal());
            assertEq("connecting", spop.getState());
            assertThrows("SOCKET-CONNECT-ERROR", \spop.continuePoll());

            {
                msg = get_random_string();

                Queue q();
                background sub () {
                    Socket ssl();
                    # bind on a random free port
                    ssl.bind(0);
                    ssl.listen();

                    q.push(ssl.getSocketInfo().port);

                    ssl.setCertificate(TestCert);
                    ssl.setPrivateKey(TestCert);

                    Socket ns = ssl.acceptSSL();
                    SocketPollOperation spop0 = ns.startPollSend(msg);
                    while (*hash<SocketPollInfo> info = spop0.continuePoll()) {
                        #printf("send info: %y\n", info);
                        Socket::poll((info,), 1s);
                    }

                    spop0 = ns.startPollSend(msg);
                    while (*hash<SocketPollInfo> info = spop0.continuePoll()) {
                        #printf("send info: %y\n", info);
                        Socket::poll((info,), 1s);
                    }

                    spop0 = ns.startPollSend(msg);
                    while (*hash<SocketPollInfo> info = spop0.continuePoll()) {
                        #printf("send info: %y\n", info);
                        Socket::poll((info,), 1s);
                    }

                    spop0 = ns.startPollSend(bigmsg);
                    while (*hash<SocketPollInfo> info = spop0.continuePoll()) {
                        #printf("send info: %y\n", info);
                        Socket::poll((info,), 1s);
                    }
                }();

                int port3 = q.get();

                target = "127.0.0.1:" + port3;
                spop = s2.startPollConnectSsl(target);
                assertEq("connect-ssl", spop.getGoal());
                assertEq("connecting", spop.getState());
                while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                    #printf("recv info: %y\n", info);
                    new_list = Socket::poll((info,), 1s);
                }
                assertEq("connected", spop.getState());
                assertTrue(spop.goalReached());
                assertNothing(spop.getOutput());
                assertEq(msg, s2.recv(msg.size()));

                spop = s2.startPollRecvString(msg.size());
                while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                    #printf("recv info: %y\n", info);
                    new_list = Socket::poll((info,), 1s);
                }
                assertEq(msg, spop.getOutput());

                spop = s2.startPollRecvBinary(msg.size());
                while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                    #printf("recv info: %y\n", info);
                    new_list = Socket::poll((info,), 1s);
                }
                assertEq(binary(msg), spop.getOutput());

                spop = s2.startPollRecvBinary(bigmsg.size());
                while (*hash<SocketPollInfo> info = spop.continuePoll()) {
                    #printf("recv info: %y\n", info);
                    new_list = Socket::poll((info,), 1s);
                }
                assertEq(bigmsg, spop.getOutput());
            }

            {
                Queue q();
                background sub () {
                    Socket ssl();
                    # bind on a random free port
                    ssl.bind(0);
                    ssl.listen();

                    q.push(ssl.getSocketInfo().port);

                    ssl.accept();
                }();

                int port3 = q.get();

                target = "127.0.0.1:" + port3;
                spop = s2.startPollConnectSsl(target);
                assertEq("connect-ssl", spop.getGoal());
                assertEq("connecting", spop.getState());
                on_error printf("goal reached: %y state: %y output: %y\n", spop.goalReached(), spop.getState(), spop.getOutput());
                assertThrows("SOCKET-SSL-ERROR", sub () {
                    *hash<SocketPollInfo> info;
                    do {
                        info = spop.continuePoll();
                        if (info) {
                            Socket::poll((info,), 250ms);
                        }
                    } while (info);
                });
                assertFalse(spop.goalReached());
                assertNothing(spop.getOutput());
            }
        }

        # bind and listen again
        if (s0.bind("127.0.0.1:0", True)) {
            throw "SOCKET-BIND-ERROR", strerror();
        }
        s0.listen();
        int port = s0.getSocketInfo().port;

        s1.connect("127.0.0.1:" + port);
        Socket s2 = s0.accept();
        string test_msg = get_random_string();
        s2.send(test_msg);

        poll_list = (
            <SocketPollInfo>{
                "events": SOCK_POLLIN,
                "socket": s1,
            },
            <SocketPollInfo>{
                "events": SOCK_POLLOUT,
                "socket": s2,
            },
        );

        new_list = Socket::poll(poll_list, 5s);
        assertEq(2, new_list.size());
        assertEq(SOCK_POLLIN, new_list[0].events);
        assertEq(s1, new_list[0].socket);
        assertEq(SOCK_POLLOUT, new_list[1].events);
        assertEq(s2, new_list[1].socket);

        poll_list = (
            <SocketPollInfo>{
                "events": SOCK_POLLIN | SOCK_POLLOUT,
                "socket": s1,
            },
            <SocketPollInfo>{
                "events": SOCK_POLLIN | SOCK_POLLOUT,
                "socket": s2,
            },
        );

        new_list = Socket::poll(poll_list, 5s);
        assertEq(2, new_list.size());
        assertEq(SOCK_POLLIN | SOCK_POLLOUT, new_list[0].events);
        assertEq(s1, new_list[0].socket);
        assertEq(SOCK_POLLOUT, new_list[1].events);
        assertEq(s2, new_list[1].socket);

        string msg = s1.recv(-1);
        assertEq(test_msg, msg);

        new_list = Socket::poll(poll_list, 5s);
        assertEq(2, new_list.size());
        assertEq(SOCK_POLLOUT, new_list[0].events);
        assertEq(s1, new_list[0].socket);
        assertEq(SOCK_POLLOUT, new_list[1].events);
        assertEq(s2, new_list[1].socket);

        s1.close();
        assertThrows("DESCRIPTOR-NOT-OPEN", \Socket::poll(), (poll_list, 0s));

        new_list = poll_list;
        remove new_list[0].socket;
        assertThrows("SOCKET-POLL-ERROR", \Socket::poll(), (new_list, 1s));

        shift poll_list;
        new_list = poll_list;
        new_list[0].events = 0;
        assertThrows("SOCKET-POLL-ERROR", \Socket::poll(), (new_list, 1s));

        delete s2;
        assertThrows("OBJECT-ALREADY-DELETED", \Socket::poll(), (poll_list, 1s));

        # close socket in background
        s1.connect("127.0.0.1:" + port);
        s2 = s0.accept();

        Counter cnt(1);
        Queue q();
        code poll = sub () {
            list<hash<SocketPollInfo>> new_poll_list = (
                <SocketPollInfo>{
                    "events": SOCK_POLLIN,
                    "socket": s1,
                },
                <SocketPollInfo>{
                    "events": SOCK_POLLIN,
                    "socket": s2,
                },
            );
            cnt.dec();
            try {
                q.push(Socket::poll(new_poll_list, 500ms));
            } catch (hash<ExceptionInfo> ex) {
                q.push(ex);
            }
        };
        background poll();
        cnt.waitForZero();
        usleep(250ms);
        delete s1;
        auto v = q.get();
        on_error {
            printf("%y\n", v.fullType());
            map printf("%d: events: %y\n", $#, $1.events), v;
        }
        assertEq(SOCK_POLLERR, v[0].events);
    }

    tls13Test() {
        assertEq(0, qore_get_library_options() & QLO_DISABLE_TLS_13);

        # disable TLS 1.3
        qore_set_library_options(QLO_DISABLE_TLS_13);
        assertEq(QLO_DISABLE_TLS_13, qore_get_library_options() & QLO_DISABLE_TLS_13);

        Queue q();
        background doServ(q);

        Socket s1();
        s1.connectSSL("127.0.0.1:" + q.get());

        if (m_options.verbose > 2) {
            SocketTest::printf("server: secure connection (%s %s) from %s\n", s1.getSSLCipherName(), s1.getSSLCipherVersion(),
                s1.getPeerInfo().address_desc);
        }
        assertNeq("TLSv1.3", s1.getSSLCipherVersion());
    }

    connectionIdTest() {
        Socket s();
        assertEq(0, s.getConnectionId());
        s.bind(0);
        s.close();
        assertEq(1, s.getConnectionId());
    }

    sslWriteDisconnectTest() {
        Queue q();
        background sslReadDisconnect(q);

        # create server socket
        Socket s();
        # bind on a random free port
        s.bind(0);
        s.listen();
        s.setCertificate(TestCert);
        s.setPrivateKey(TestCert);
        # send the port to the client
        q.push(s.getSocketInfo().port);
        # accept a new connection
        Socket ns = s.acceptSSL(15s);

        hash<ExceptionInfo> ex1;
        while (True) {
            try {
                ns.send(TestCert);
            } catch (hash<ExceptionInfo> ex) {
                ex1 = ex;
                break;
            }
        }
        #printf("%s: %s\n", ex1.err, ex1.desc);
        assertEq("SOCKET-SSL-ERROR", ex1.err);
    }

    sslReadDisconnect(Queue q) {
        Socket s();
        int port = q.get();
        s.connectSSL("127.0.0.1:" + port);

        # get one byte
        s.recvu1();
        # and disconnect
    }

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

        Socket s();
        # wait for server to be listening and get port
        int port = q.get();
        s.connectSSL("127.0.0.1:" + port);
        assertThrows("SOCKET-CLOSED", \s.readHTTPHeader());
    }

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

    randomPortSocketTest() {
        Socket s();
        # bind on a random port
        s.bindINET("127.0.0.1", 0);
        # get port bound
        hash h = s.getSocketInfo();
        if (m_options.verbose > 2)
            printf("port: %d\n", h.port);

        assertEq(True, h.port > 0);
        assertEq(h.port, s.getPort());
        if (s.listen())
            throw "LISTEN-ERROR", strerror();

        Counter c(1);

        string str = "hello";
        code sendMsg = sub () {
            Socket ns();
            ns.connectINET("127.0.0.1", h.port, 10s);
            ns.send(str);
            assertEq(str, ns.recv(str.size()));
            c.dec();
        };
        background sendMsg();

        Socket ns = s.accept(10s);
        assertEq(str, ns.recv(str.size()));
        ns.send(str);
        c.waitForZero();
    }

    unconnectedSocketTest() {
        Socket s();
        assertThrows("SOCKET-NOT-OPEN", \s.upgradeClientToSSL());
        assertThrows("SOCKET-NOT-OPEN", \s.upgradeServerToSSL());
        assertThrows("SOCKET-NOT-OPEN", \s.getPeerInfo());
        assertThrows("SOCKET-NOT-OPEN", \s.getSocketInfo());
        assertThrows("SOCKET-NOT-OPEN", \s.recv(), 1);
        assertThrows("SOCKET-NOT-OPEN", \s.recvi1());
        assertThrows("SOCKET-NOT-OPEN", \s.recvBinary(), 1);
        assertThrows("SOCKET-NOT-OPEN", \s.send(), "1");
        assertThrows("SOCKET-NOT-OPEN", \s.sendi1(), 1);

        assertEq(NOTHING, s.verifyPeerCertificate());
        assertEq(NOTHING, s.getSSLCipherName());
        assertEq(NOTHING, s.getSSLCipherVersion());
    }

    clientServerSocketTest() {
        # create event queue and start listening thread if necessary
        if (m_options.events)
            background listen();

        if (!exists m_options.server) {
            stopc.inc();
            background server_thread();
        }
        if (!m_options.servonly) {
            stopc.inc();
            background client_thread();
        }

        stopc.waitForZero();
        if (m_options.events)
            queue.push();
    }

    private server_thread() {
        on_exit stopc.dec();
        #SocketTest::printf("listening for incoming connections on %s\n", server_port);
        Socket s();
        # setting the callback will output far too much data
        if (m_options.events) {
            s.setEventQueue(queue, "server", True);
        }

        {
            on_exit
                counter.dec();

            # get bind addresses for the local host
            list addr = getaddrinfo(NOTHING, server_port, fam, AI_PASSIVE);

            # sort ipv6 addresses first in list
            addr = sort(addr, int sub (hash l, hash r) { return l.family === AF_INET6 ? -1 : 0; });

            foreach hash a in (addr) {
                try {
                    s.bindINET(a.address, server_port, True, a.family);
                    #SocketTest::printf("server: bound to %s socket on %s:%d\n", a.familystr, a.address_desc, server_port);
                    break;
                } catch (hash<ExceptionInfo> ex) {
                    if ($# == (elements addr - 1)) {
                        #SocketTest::printf("server: error binding socket to %s:%d %s: %s (arg: %y)\n", a.address_desc, server_port, ex.err, ex.desc, ex.arg);
                        thread_exit;
                    }
                }
            }

            if (s.listen()) {
                SocketTest::printf("listen error (%s)\n", strerror(errno()));
                thread_exit;
            }
        }

        try {
            if (m_options.ssl) {
                if (m_options.cert) {
                    File f();
                    f.open2(m_options.cert);
                    SSLCertificate cert(f.read(-1));
                    s.setCertificate(cert);
                    if (m_options.key) {
                        f.open2(m_options.key);
                        s.setPrivateKey(new SSLPrivateKey(f.read(-1), m_options.pass));
                    }
                }
                s = s.acceptSSL();
                #SocketTest::printf("returned from Socket::acceptSSL() s=%N\n", s);
                SocketTest::printf("server: secure connection (%s %s) from %s\n", s.getSSLCipherName(), s.getSSLCipherVersion(), s.getPeerInfo().address_desc);
                *string str = s.verifyPeerCertificate();
                if (!str)
                    SocketTest::printf("server: no client certificate\n");
                else
                    SocketTest::printf("server: client certificate: %n %s: %s\n", str, X509_VerificationReasons{str});
            } else {
                s = s.accept();
                #SocketTest::printf("server: cleartext connection from %s\n", s.getPeerInfo().address_desc);
            }
            if (m_options.events)
                s.setEventQueue(queue, "server connection", True);
        } catch (hash<ExceptionInfo> ex) {
            SocketTest::printf("server error: %s: %s: %s\n", get_ex_pos(ex), ex.err, ex.desc);
            thread_exit;
        }

        receive_messages(s, "server");
        send_messages(s);

        s.close();
    }

    private client_thread() {
        on_exit stopc.dec();
        if (!exists m_options.server)
            counter.waitForZero();
        Socket s();
        # setting the callback will output far too much data
        if (m_options.events)
            s.setEventQueue(queue, "client", True);

        try {
            if (m_options.ssl) {
                if (strlen(m_options.clientcert)) {
                    s.setCertificate(m_options.clientcert);
                    if (!strlen(m_options.clientkey))
                        s.setPrivateKey(m_options.clientcert);
                }
                if (strlen(m_options.clientkey))
                    s.setPrivateKey(m_options.clientkey);
                s.connectINETSSL(client_host, client_port, 15s, fam);

                string str = s.verifyPeerCertificate();
                SocketTest::printf("client: server certificate: %s: %s\n", str, X509_VerificationReasons{str});
            }
            else
                s.connectINET(client_host, client_port, 15s, fam);
        } catch (hash<ExceptionInfo> ex) {
            SocketTest::printf("client error %s: %s: %s\n", get_ex_pos(ex), ex.err, ex.desc);
            thread_exit;
        }

        send_messages(s);
        receive_messages(s, "client");
    }

    private receive_messages(Socket s, string who) {
        auto m = s.recv();
        assertEq(XString, m, who + " string");
        s.send("OK");

        m = s.recvBinary();
        assertEq(XBinary, m, who + " binary");
        s.send("OK");

        m = s.recvi1();
        assertEq(i1, m, who + " sendi1");
        s.send("OK");

        m = s.recvi2();
        assertEq(i2, m, who + " sendi2");
        s.send("OK");

        m = s.recvi4();
        assertEq(i4, m, who + " sendi4");
        s.send("OK");

        m = s.recvi8();
        assertEq(i8, m, who + " sendi8");
        s.send("OK");

        m = s.recvi2LSB();
        assertEq(i2, m, who + " sendi2LSB");
        s.send("OK");

        m = s.recvi4LSB();
        assertEq(i4, m, who + " sendi4LSB");
        s.send("OK");

        m = s.recvi8LSB();
        assertEq(i8, m, who + " sendi8LSB");
        s.send("OK");

        hash<auto> info;
        m = s.readHTTPHeader(-1, \info);
        assertEq(Type::Hash, info."headers-raw".type());
        assertEq("POST", m.method, who + " HTTP header method");
        m = s.recv(m."content-length");
        assertEq(XString, m, who + " HTTP message body");

        s.sendHTTPResponse(200, "OK", "1.1", http_headers, "OK", \info);
        assertEq("HTTP/1.1 200 OK", info."response-uri");

        m = s.readHTTPHeader(-1, \info);
        assertEq(Type::Hash, info."headers-raw".type());
        assertEq("GET", m.method, who + " HTTP header method");
        assertEq("0", m."content-length");

        StringInputStream input_stream("OK");
        s.sendHTTPResponse(200, "OK", "1.1", http_headers + {"Transfer-Encoding": "chunked"}, input_stream, NOTHING, \info);
        assertEq("HTTP/1.1 200 OK", info."response-uri");
    }

    private send_messages(Socket s) {
        SocketTest::check_send(s.send(XString), "string");
        get_response(s);

        SocketTest::check_send(s.send(XBinary), "binary");
        get_response(s);

        SocketTest::check_send(s.sendi1(i1), "i1");
        get_response(s);
        SocketTest::check_send(s.sendi2(i2), "i2");
        get_response(s);
        SocketTest::check_send(s.sendi4(i4), "i4");
        get_response(s);
        SocketTest::check_send(s.sendi8(i8), "i8");
        get_response(s);

        SocketTest::check_send(s.sendi2LSB(i2), "i2LSB");
        get_response(s);
        SocketTest::check_send(s.sendi4LSB(i4), "i4LSB");
        get_response(s);
        SocketTest::check_send(s.sendi8LSB(i8), "i8LSB");
        get_response(s);
        s.sendHTTPMessage("POST", "none", "1.1", http_headers, XString);
        get_http_response(s);
        s.sendHTTPMessage("GET", "none", "1.1", http_headers);
        get_http_response(s);
    }

    static check_send(int rc, string data) {
        if (rc == -1)
            printf("%s send error: %s\n", data, strerror(errno()));
    }

    private get_response(Socket s) {
        string m = s.recv(2);
        if (m != "OK")
            throw "RESPONSE-ERROR", sprintf("expecting 'OK', got: %N", m);
    }

    private get_http_response(Socket s) {
        auto m = s.readHTTPHeader();
        if (m."transfer-encoding" == "chunked") {
            m = s.readHTTPChunkedBody();
            assertEq("OK", m.body);
        } else {
            m = s.recv(m."content-length");
            if (m != "OK")
                throw "RESPONSE-ERROR", sprintf("expecting 'OK', got: %N", m);
        }
    }

    private usageIntern(int offset = OffsetColumn) {
        TestReporter::usageIntern();
        SocketTest::printf("usage: %s -[options] [port]
  -h,--help                    this help text
  -S,--server=ip:port          no server thread; connect to remote server
  -O,--server-only             no client thread; wait for remote clients
  -4,--ipv4                    ipv4 sockets only
  -6,--ipv6                    ipv6 sockets only
  -s,--ssl                     use secure connections
  -c,--cert=arg                set server SSL x509 certificate
  -k,--private-key=arg         set server SSL private key
  -C,--client-cert=arg         set client SSL x509 certificate
  -K,--client-private-key=arg  set client SSL private key
  -e,--show-events             monitor socket events
", basename(ENV."_"));
    }

    private process_command_line() {
        #if (m_options.help)
        #    SocketTest::usage();

        if (exists m_options.server && m_options.servonly) {
            SocketTest::printf("server only flag set and remote server option=%n set - aborting\n", m_options.server);
            exit(1);
        }

        if (!(server_port = int(shift ARGV)))
            server_port = 9001;

        if (exists m_options.server) {
            if (m_options.server == int(m_options.server)) {
                client_port = int(m_options.server);
                client_host = "127.0.0.1";
            } else {
                hash h = parse_url(m_options.server);
                if (!exists h.port) {
                    SocketTest::printf("missing port in server specification %n - aborting\n", m_options.server);
                    exit(1);
                }

                client_port = h.port;
                if (!exists h.host)
                    client_host = "127.0.0.1";
                else
                    client_host = h.host;
            }
        } else {
            client_port = server_port;
            client_host = "127.0.0.1";
        }

        # set address family for sockets
        if (m_options.ipv4 && !m_options.ipv6)
            fam = AF_INET;
        else if (m_options.ipv6 && !m_options.ipv4)
            fam = AF_INET6;
        else
            fam = AF_UNSPEC;
    }

    private listen() {
        while (True) {
            # get a message from the event queue; a hash is returned with at
            # least the following keys: "event" with the event code, and
            # "source" with the event source identifier

            *hash<auto> e = queue.get();

            # stop listening when empty event posted to queue in constructor()
            if (!e)
                return;

            if (e.data.typeCode() == NT_BINARY) {
                string str = e.data.toString();
                delete e.data;
                for (int i = 0, int end = str.size(); i < end; ++i) {
                    string char = str[i];
                    int uc = char.getUnicode();
                    if (uc < 32 || uc >= 127) {
                        e.data += sprintf("<%x>", uc);
                    } else {
                        e.data += char;
                    }
                }
            }

            e.id = sprintf("%x", e.id);

            SocketTest::printf("%s %s: %s\n", EVENT_SOURCE_MAP.(e.source), EVENT_MAP.(e.event), SocketTest::getstr(e - ("event", "source")));
            flush(); # flush output
        }
    }

    # ensure all output is synchronized
    synchronized static printf(string fmt) {
        vprintf(fmt, argv);
    }

    static string getstr(hash h) {
        string str;

        # we create the string by mapping our string format function on a list
        # of the hash keys.  this is more consise than iterating the keys with
        # a "foreach" statement, for example

        map (str += sprintf("%s: %y ", $1.key, $1.value)), h.pairIterator();
        return str;
    }
}
