/*
 * This file is open source software, licensed to you under the terms
 * of the Apache License, Version 2.0 (the "License").  See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership.  You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/*
 * Copyright (C) 2015 Cloudius Systems, Ltd.
 */

#include <iostream>

#include <seastar/core/do_with.hh>
#include <seastar/core/sstring.hh>
#include <seastar/core/reactor.hh>
#include <seastar/core/loop.hh>
#include <seastar/core/sharded.hh>
#include <seastar/core/thread.hh>
#include <seastar/core/gate.hh>
#include <seastar/core/temporary_buffer.hh>
#include <seastar/core/iostream.hh>
#include <seastar/core/with_timeout.hh>
#include <seastar/util/std-compat.hh>
#include <seastar/util/process.hh>
#include <seastar/net/tls.hh>
#include <seastar/net/dns.hh>
#include <seastar/net/inet_address.hh>
#include <seastar/testing/test_case.hh>
#include <seastar/testing/thread_test_case.hh>

#include <boost/dll.hpp>

#include "loopback_socket.hh"
#include "tmpdir.hh"

#include <gnutls/gnutls.h>

#if 0

static void enable_gnutls_logging() {
    gnutls_global_set_log_level(99);
        gnutls_global_set_log_function([](int lv, const char * msg) {
           std::cerr << "GNUTLS (" << lv << ") " << msg << std::endl;
        });
}
#endif

static const auto cert_location = boost::dll::program_location().parent_path();

static std::string certfile(const std::string& file) {
    return (cert_location / file).string();
}

using namespace seastar;

static future<> connect_to_ssl_addr(::shared_ptr<tls::certificate_credentials> certs, socket_address addr, const sstring& name = {}) {
    return repeat_until_value([=]() mutable {
        return tls::connect(certs, addr, tls::tls_options{.server_name = name}).then([](connected_socket s) {
            return do_with(std::move(s), [](connected_socket& s) {
                return do_with(s.output(), [&s](auto& os) {
                    static const sstring msg("GET / HTTP/1.0\r\n\r\n");
                    auto f = os.write(msg);
                    return f.then([&s, &os]() mutable {
                        auto f = os.flush();
                        return f.then([&s]() mutable {
                            return do_with(s.input(), sstring{}, [](auto& in, sstring& buffer) {
                                return do_until(std::bind(&input_stream<char>::eof, std::cref(in)), [&buffer, &in] {
                                    auto f = in.read();
                                    return f.then([&](temporary_buffer<char> buf) {
                                        buffer.append(buf.get(), buf.size());
                                    });
                                }).then([&buffer]() -> future<std::optional<bool>> {
                                    if (buffer.empty()) {
                                        // # 1127 google servers have a (pretty short) timeout between connect and expected first
                                        // write. If we are delayed inbetween connect and write above (cert verification, scheduling
                                        // solar spots or just time sharing on AWS) we could get a short read here. Just retry.
                                        // If we get an actual error, it is either on protocol level (exception) or HTTP error.
                                        return make_ready_future<std::optional<bool>>(std::nullopt);
                                    }
                                    BOOST_CHECK(buffer.size() > 8);
                                    BOOST_CHECK_EQUAL(buffer.substr(0, 5), sstring("HTTP/"));
                                    return make_ready_future<std::optional<bool>>(true);
                                });
                            });
                        });
                    }).finally([&os] {
                        return os.close();
                    });
                });
            });
        });

    }).discard_result();
}

#if SEASTAR_TESTING_WITH_NETWORKING

static const auto google_name = "www.google.com";

// broken out from below. to allow pre-lookup
static future<socket_address> google_address() {
    static socket_address google;

    if (google.is_unspecified()) {
        return net::dns::resolve_name(google_name, net::inet_address::family::INET).then([](net::inet_address addr) {
            google = socket_address(addr, 443);
            return google_address();
        });
    }
    return make_ready_future<socket_address>(google);
}

static future<> connect_to_ssl_google(::shared_ptr<tls::certificate_credentials> certs) {
    return google_address().then([certs](socket_address addr) {
        return connect_to_ssl_addr(std::move(certs), addr, google_name);
    });
}

SEASTAR_TEST_CASE(test_simple_x509_client) {
    auto certs = ::make_shared<tls::certificate_credentials>();
    return certs->set_x509_trust_file(certfile("tls-ca-bundle.pem"), tls::x509_crt_format::PEM).then([certs]() {
        return connect_to_ssl_google(certs);
    });
}

SEASTAR_TEST_CASE(test_x509_client_with_system_trust) {
    auto certs = ::make_shared<tls::certificate_credentials>();
    return certs->set_system_trust().then([certs]() {
        return connect_to_ssl_google(certs);
    });
}

SEASTAR_TEST_CASE(test_x509_client_with_builder_system_trust) {
    tls::credentials_builder b;
    (void)b.set_system_trust();
    return connect_to_ssl_google(b.build_certificate_credentials());
}

SEASTAR_TEST_CASE(test_x509_client_with_builder_system_trust_multiple) {
    // avoid getting parallel connects stuck on dns lookup (if running single case).
    // pre-lookup www.google.com
    return google_address().then([](socket_address) {
        tls::credentials_builder b;
        (void)b.set_system_trust();
        auto creds = b.build_certificate_credentials();

        return parallel_for_each(boost::irange(0, 20), [creds](auto i) { return connect_to_ssl_google(creds); });
    });
}

SEASTAR_TEST_CASE(test_x509_client_with_system_trust_and_priority_strings) {
    static std::vector<sstring> prios( {
        "NORMAL:+ARCFOUR-128", // means normal ciphers plus ARCFOUR-128.
        "SECURE128:-VERS-SSL3.0:+COMP-DEFLATE", // means that only secure ciphers are enabled, SSL3.0 is disabled, and libz compression enabled.
        "SECURE256:+SECURE128",
        "NORMAL:%COMPAT",
        "NORMAL:-MD5",
        "NONE:+VERS-TLS-ALL:+MAC-ALL:+RSA:+AES-128-CBC:+SIGN-ALL:+COMP-NULL",
        "NORMAL:+ARCFOUR-128",
        "SECURE128:-VERS-TLS1.0:+COMP-DEFLATE",
        "SECURE128:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2"
    });
    return do_for_each(prios, [](const sstring & prio) {
        tls::credentials_builder b;
        (void)b.set_system_trust();
        b.set_priority_string(prio);
        return connect_to_ssl_google(b.build_certificate_credentials());
    });
}

SEASTAR_TEST_CASE(test_x509_client_with_system_trust_and_priority_strings_fail) {
    static std::vector<sstring> prios( { "NONE",
        "NONE:+CURVE-SECP256R1"
    });
    return do_for_each(prios, [](const sstring & prio) {
        tls::credentials_builder b;
        (void)b.set_system_trust();
        b.set_priority_string(prio);
        try {
            return connect_to_ssl_google(b.build_certificate_credentials()).then([] {
                BOOST_FAIL("Expected exception");
            }).handle_exception([](auto ep) {
                // ok.
            });
        } catch (...) {
            // also ok
        }
        return make_ready_future<>();
    });
}
#endif // SEASTAR_TESTING_WITH_NETWORKING

class https_server {
    const sstring _cert;
    const std::string _addr = "127.0.0.1";
    experimental::process _process;
    uint16_t _port;

    static experimental::process spawn(const std::string& addr, const sstring& key, const sstring& cert) {
        auto httpd = boost::dll::program_location().parent_path() / "https-server.py";
        const std::vector<sstring> argv{
          "httpd",
          "--server", fmt::format("{}:{}", addr, 0),
          "--key", key,
          "--cert", cert,
        };
        return experimental::spawn_process(httpd.string(), {.argv = argv}).get();
    }

    // https-server.py picks an available port and listens on it. when it is
    // ready to serve, it prints out the listening port. without hardwiring to
    // a fixed port, we are able to run multiple tests in parallel.
    static uint16_t read_port(experimental::process& process) {
        using consumption_result_type = typename input_stream<char>::consumption_result_type;
        using stop_consuming_type = typename consumption_result_type::stop_consuming_type;
        using tmp_buf = stop_consuming_type::tmp_buf;
        struct consumer {
            future<consumption_result_type> operator()(tmp_buf buf) {
                if (auto newline = std::find(buf.begin(), buf.end(), '\n'); newline != buf.end()) {
                    size_t consumed = newline - buf.begin();
                    line += std::string_view(buf.get(), consumed);
                    buf.trim_front(consumed);
                    return make_ready_future<consumption_result_type>(stop_consuming_type(std::move(buf)));
                } else {
                    line += std::string_view(buf.get(), buf.size());
                    return make_ready_future<consumption_result_type>(stop_consuming_type({}));
                }
            }
            std::string line;
        };
        auto reader = ::make_shared<consumer>();
        process.cout().consume(*reader).get();
        return std::stoul(reader->line);
    }

public:
    https_server(const std::string& ca = "mtls_ca")
        : _cert(certfile(fmt::format("{}.crt", ca)))
        , _process(spawn(_addr, certfile(fmt::format("{}.key", ca)), _cert))
        , _port(read_port(_process))
    {}
    ~https_server() {
        _process.terminate();
        _process.wait().discard_result().get();
    }
    const sstring& cert() const {
        return _cert;
    }
    socket_address addr() const {
        return ipv4_addr(_addr, _port);
    }
    sstring name() const {
        // should be identical to the one passed as the "-addext" option when
        // generating the cert.
        return "127.0.0.1";
    }
};

#if !SEASTAR_TESTING_WITH_NETWORKING

SEASTAR_THREAD_TEST_CASE(test_simple_x509_client) {
    auto certs = ::make_shared<tls::certificate_credentials>();
    https_server server;
    certs->set_x509_trust_file(server.cert(), tls::x509_crt_format::PEM).get();
    connect_to_ssl_addr(certs, server.addr(), server.name()).get();
}

#endif // !SEASTAR_TESTING_WITH_NETWORKING

SEASTAR_THREAD_TEST_CASE(test_x509_client_with_builder) {
    tls::credentials_builder b;
    https_server server;
    b.set_x509_trust_file(server.cert(), tls::x509_crt_format::PEM).get();
    connect_to_ssl_addr(b.build_certificate_credentials(), server.addr()).get();
}

SEASTAR_THREAD_TEST_CASE(test_x509_client_with_builder_multiple) {
    tls::credentials_builder b;
    https_server server;
    b.set_x509_trust_file(server.cert(), tls::x509_crt_format::PEM).get();
    auto creds = b.build_certificate_credentials();
    auto addr = server.addr();
    parallel_for_each(boost::irange(0, 20), [creds, addr](auto i) {
        return connect_to_ssl_addr(creds, addr);
    }).get();
}

SEASTAR_THREAD_TEST_CASE(test_x509_client_with_priority_strings) {
    static std::vector<sstring> prios( {
        "NORMAL:+ARCFOUR-128", // means normal ciphers plus ARCFOUR-128.
        "SECURE128:-VERS-SSL3.0:+COMP-DEFLATE", // means that only secure ciphers are enabled, SSL3.0 is disabled, and libz compression enabled.
        "SECURE256:+SECURE128",
        "NORMAL:%COMPAT",
        "NORMAL:-MD5",
        "NONE:+VERS-TLS-ALL:+MAC-ALL:+RSA:+AES-256-GCM:+SIGN-ALL:+COMP-NULL:+GROUP-EC-ALL",
        "NORMAL:+ARCFOUR-128",
        "SECURE128:-VERS-TLS1.0:+COMP-DEFLATE",
        "SECURE128:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.2"
    });
    tls::credentials_builder b;
    https_server server;
    b.set_x509_trust_file(server.cert(), tls::x509_crt_format::PEM).get();
    auto addr = server.addr();
    do_for_each(prios, [&b, addr](const sstring& prio) {
        b.set_priority_string(prio);
        return connect_to_ssl_addr(b.build_certificate_credentials(), addr);
    }).get();
}

SEASTAR_THREAD_TEST_CASE(test_x509_client_with_priority_strings_fail) {
    static std::vector<sstring> prios( { "NONE",
        "NONE:+CURVE-SECP256R1"
    });
    tls::credentials_builder b;
    https_server server;
    b.set_x509_trust_file(server.cert(), tls::x509_crt_format::PEM).get();
    auto addr = server.addr();
    do_for_each(prios, [&b, addr](const sstring& prio) {
        b.set_priority_string(prio);
        try {
            return connect_to_ssl_addr(b.build_certificate_credentials(), addr).then([] {
                BOOST_FAIL("Expected exception");
            }).handle_exception([](auto ep) {
                // ok.
            });
        } catch (...) {
            // also ok
        }
        return make_ready_future<>();
    }).get();
}

SEASTAR_TEST_CASE(test_failed_connect) {
    tls::credentials_builder b;
    (void)b.set_system_trust();
    return connect_to_ssl_addr(b.build_certificate_credentials(), ipv4_addr()).handle_exception([](auto) {});
}

SEASTAR_TEST_CASE(test_non_tls) {
    ::listen_options opts;
    opts.reuse_address = true;
    auto addr = ::make_ipv4_address( {0x7f000001, 4712});
    auto server = server_socket(seastar::listen(addr, opts));

    auto c = server.accept();

    tls::credentials_builder b;
    (void)b.set_system_trust();

    auto f = connect_to_ssl_addr(b.build_certificate_credentials(), addr);


    return c.then([f = std::move(f)](accept_result ar) mutable {
        ::connected_socket s = std::move(ar.connection);
        std::cerr << "Established connection" << std::endl;
        auto sp = std::make_unique<::connected_socket>(std::move(s));
        timer<> t([s = std::ref(*sp)] {
            std::cerr << "Killing server side" << std::endl;
            s.get() = ::connected_socket();
        });
        t.arm(timer<>::clock::now() + std::chrono::seconds(5));
        return std::move(f).finally([t = std::move(t), sp = std::move(sp)] {});
    }).handle_exception([server = std::move(server)](auto ep) {
        std::cerr << "Got expected exception" << std::endl;
    });
}

SEASTAR_TEST_CASE(test_abort_accept_before_handshake) {
    auto certs = ::make_shared<tls::server_credentials>(::make_shared<tls::dh_params>());
    return certs->set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).then([certs] {
        ::listen_options opts;
        opts.reuse_address = true;
        auto addr = ::make_ipv4_address( {0x7f000001, 4712});
        auto server = server_socket(tls::listen(certs, addr, opts));
        auto c = server.accept();
        BOOST_CHECK(!c.available()); // should not be finished

        server.abort_accept();

        return c.then([](auto) { BOOST_FAIL("Should not reach"); }).handle_exception([](auto) {
            // ok
        }).finally([server = std::move(server)] {});
    });
}

SEASTAR_TEST_CASE(test_abort_accept_after_handshake) {
    return async([] {
        auto certs = ::make_shared<tls::server_credentials>(::make_shared<tls::dh_params>());
        certs->set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();

        ::listen_options opts;
        opts.reuse_address = true;
        auto addr = ::make_ipv4_address( {0x7f000001, 4712});
        auto server = tls::listen(certs, addr, opts);
        auto sa = server.accept();

        tls::credentials_builder b;
        b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();

        auto c = tls::connect(b.build_certificate_credentials(), addr).get();
        auto s = sa.get();
        server.abort_accept(); // should not affect the socket we got.
        auto out = c.output();
        auto in = s.connection.input();

        out.write("apa").get();
        auto f = out.flush();
        auto buf = in.read().get();
        f.get();
        BOOST_CHECK(sstring(buf.begin(), buf.end()) == "apa");

        out.close().get();
        in.close().get();
    });
}

SEASTAR_TEST_CASE(test_abort_accept_on_server_before_handshake) {
    return async([] {
        ::listen_options opts;
        opts.reuse_address = true;
        auto addr = ::make_ipv4_address( {0x7f000001, 4712});
        auto server = server_socket(seastar::listen(addr, opts));
        auto sa = server.accept();

        tls::credentials_builder b;
        b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();

        auto creds = b.build_certificate_credentials();
        auto f = tls::connect(creds, addr);

        server.abort_accept();
        try {
            sa.get();
        } catch (...) {
        }
        server = {};

        try {
            // the connect as such should succeed, but the handshare following it
            // should not.
            auto c = f.get();
            auto out = c.output();
            out.write("apa").get();
            out.flush().get();
            out.close().get();

            BOOST_FAIL("Expected exception");
        } catch (...) {
            // ok
        }
    });
}


struct streams {
    ::connected_socket s;
    input_stream<char> in;
    output_stream<char> out;

    // note: using custom output_stream, because we don't want polled flush
    streams(::connected_socket cs) : s(std::move(cs)), in(s.input()), out(s.output().detach(), 8192)
    {}
};

static const sstring message = "hej lilla fisk du kan dansa fint";

class echoserver {
    ::server_socket _socket;
    ::shared_ptr<tls::server_credentials> _certs;
    seastar::gate _gate;
    bool _stopped = false;
    size_t _size;
    std::exception_ptr _ex;
public:
    echoserver(size_t message_size, bool use_dh_params = true)
            : _certs(
                    use_dh_params 
                        ? ::make_shared<tls::server_credentials>(::make_shared<tls::dh_params>())
                        : ::make_shared<tls::server_credentials>()
                    )
            , _size(message_size)
    {}

    future<> listen(socket_address addr, sstring crtfile, sstring keyfile, tls::client_auth ca = tls::client_auth::NONE, sstring trust = {}) {
        _certs->set_client_auth(ca);
        auto f = _certs->set_x509_key_file(crtfile, keyfile, tls::x509_crt_format::PEM);
        if (!trust.empty()) {
            f = f.then([this, trust = std::move(trust)] {
                return _certs->set_x509_trust_file(trust, tls::x509_crt_format::PEM);
            });
        }
        return f.then([this, addr] {
            ::listen_options opts;
            opts.reuse_address = true;

            _socket = tls::listen(_certs, addr, opts);

            (void)try_with_gate(_gate, [this] {
                return _socket.accept().then([this](accept_result ar) {
                    ::connected_socket s = std::move(ar.connection);
                    auto strms = ::make_lw_shared<streams>(std::move(s));
                    return repeat([strms, this]() {
                        return strms->in.read_exactly(_size).then([strms](temporary_buffer<char> buf) {
                            if (buf.empty()) {
                                return make_ready_future<stop_iteration>(stop_iteration::yes);
                            }
                            sstring tmp(buf.begin(), buf.end());
                            return strms->out.write(tmp).then([strms]() {
                                return strms->out.flush();
                            }).then([] {
                                return make_ready_future<stop_iteration>(stop_iteration::no);
                            });
                        });
                    }).finally([strms]{
                        return strms->out.close();
                    }).finally([strms]{});
                }).handle_exception([this](auto ep) {
                    if (_stopped) {
                        return make_ready_future<>();
                    }
                    _ex = ep;
                    return make_ready_future<>();
                });
            }).handle_exception_type([] (const gate_closed_exception&) {/* ignore */});
            return make_ready_future<>();
        });
    }

    future<> stop() {
        _stopped = true;
        _socket.abort_accept();
        return _gate.close().handle_exception([this] (std::exception_ptr ignored) {
            if (_ex) {
                std::rethrow_exception(_ex);
            }
        });
    }
};

static future<> run_echo_test(sstring message,
                int loops,
                sstring trust,
                sstring name,
                sstring crt = certfile("test.crt"),
                sstring key = certfile("test.key"),
                tls::client_auth ca = tls::client_auth::NONE,
                sstring client_crt = {},
                sstring client_key = {},
                bool do_read = true,
                bool use_dh_params = true,
                tls::dn_callback distinguished_name_callback = {}
)
{
    static const auto port = 4711;

    auto msg = ::make_shared<sstring>(std::move(message));
    auto certs = ::make_shared<tls::certificate_credentials>();
    auto server = ::make_shared<seastar::sharded<echoserver>>();
    auto addr = ::make_ipv4_address( {0x7f000001, port});

    assert(do_read || loops == 1);

    future<> f = make_ready_future();

    if (!client_crt.empty() && !client_key.empty()) {
        f = certs->set_x509_key_file(client_crt, client_key, tls::x509_crt_format::PEM);
        if (distinguished_name_callback) {
            certs->set_dn_verification_callback(std::move(distinguished_name_callback));
        }
    }

    return f.then([=] {
        return certs->set_x509_trust_file(trust, tls::x509_crt_format::PEM);
    }).then([=] {
        return server->start(msg->size(), use_dh_params).then([=]() {
            sstring server_trust;
            if (ca != tls::client_auth::NONE) {
                server_trust = trust;
            }
            return server->invoke_on_all(&echoserver::listen, addr, crt, key, ca, server_trust);
        }).then([=] {
            return tls::connect(certs, addr, tls::tls_options{.server_name=name}).then([loops, msg, do_read](::connected_socket s) {
                auto strms = ::make_lw_shared<streams>(std::move(s));
                auto range = boost::irange(0, loops);
                return do_for_each(range, [strms, msg](auto) {
                    auto f = strms->out.write(*msg);
                    return f.then([strms, msg]() {
                        return strms->out.flush().then([strms, msg] {
                            return strms->in.read_exactly(msg->size()).then([msg](temporary_buffer<char> buf) {
                                if (buf.empty()) {
                                    throw std::runtime_error("Unexpected EOF");
                                }
                                sstring tmp(buf.begin(), buf.end());
                                BOOST_CHECK(*msg == tmp);
                            });
                        });
                    });
                }).then_wrapped([strms, do_read] (future<> f1) {
                    // Always call close()
                    return (do_read ? strms->out.close() : make_ready_future<>()).then_wrapped([strms, f1 = std::move(f1)] (future<> f2) mutable {
                        // Verification errors will be reported by the call to output_stream::close(),
                        // which waits for the flush to actually happen. They can also be reported by the
                        // input_stream::read_exactly() call. We want to keep only one and avoid nested exception mess.
                        if (f1.failed()) {
                            (void)f2.handle_exception([] (std::exception_ptr ignored) { });
                            return std::move(f1);
                        }
                        (void)f1.handle_exception([] (std::exception_ptr ignored) { });
                        return f2;
                    }).finally([strms] { });
                });
            });
        }).finally([server] {
            return server->stop().finally([server]{});
        });
    });
}

/*
 * Certificates:
 *
 * make -f tests/unit/mkcert.gmk domain=scylladb.org server=test
 *
 * ->   test.crt
 *      test.csr
 *      catest.pem
 *      catest.key
 *
 * catest == snakeoil root authority for these self-signed certs
 *
 */
SEASTAR_TEST_CASE(test_simple_x509_client_server) {
    // Make sure we load our own auth trust pem file, otherwise our certs
    // will not validate
    // Must match expected name with cert CA or give empty name to ignore
    // server name
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org");
}

SEASTAR_TEST_CASE(test_simple_x509_client_server_again) {
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org");
}

#if GNUTLS_VERSION_NUMBER >= 0x030600
// Test #769 - do not set dh_params in server certs - let gnutls negotiate.
SEASTAR_TEST_CASE(test_simple_server_default_dhparams) {
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org",
        certfile("test.crt"), certfile("test.key"), tls::client_auth::NONE,
        {}, {}, true, /* use_dh_params */ false
    );
}
#endif

SEASTAR_TEST_CASE(test_x509_client_server_cert_validation_fail) {
    // Load a real trust authority here, which our certs are _not_ signed with.
    return run_echo_test(message, 1, certfile("tls-ca-bundle.pem"), {}).then([] {
            BOOST_FAIL("Should have gotten validation error");
    }).handle_exception([](auto ep) {
        try {
            std::rethrow_exception(ep);
        } catch (tls::verification_error& e) {
            // Verify exception contains info on subject/issuer
            BOOST_REQUIRE_NE(sstring(e.what()).find("Issuer"), sstring::npos);
            BOOST_REQUIRE_NE(sstring(e.what()).find("Subject"), sstring::npos);
            // ok.
        } catch (...) {
            BOOST_FAIL("Unexpected exception");
        }
    });
}

SEASTAR_TEST_CASE(test_x509_client_server_cert_validation_fail_name) {
    // Use trust store with our signer, but wrong host name
    return run_echo_test(message, 1, certfile("tls-ca-bundle.pem"), "nils.holgersson.gov").then([] {
            BOOST_FAIL("Should have gotten validation error");
    }).handle_exception([](auto ep) {
        try {
            std::rethrow_exception(ep);
        } catch (tls::verification_error&) {
            // ok.
        } catch (...) {
            BOOST_FAIL("Unexpected exception");
        }
    });
}

SEASTAR_TEST_CASE(test_large_message_x509_client_server) {
    // Make sure we load our own auth trust pem file, otherwise our certs
    // will not validate
    // Must match expected name with cert CA or give empty name to ignore
    // server name
    sstring msg = uninitialized_string(512 * 1024);
    for (size_t i = 0; i < msg.size(); ++i) {
        msg[i] = '0' + char(i % 30);
    }
    return run_echo_test(std::move(msg), 20, certfile("catest.pem"), "test.scylladb.org");
}

SEASTAR_TEST_CASE(test_simple_x509_client_server_fail_client_auth) {
    // Make sure we load our own auth trust pem file, otherwise our certs
    // will not validate
    // Must match expected name with cert CA or give empty name to ignore
    // server name
    // Server will require certificate auth. We supply none, so should fail connection
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org", certfile("test.crt"), certfile("test.key"), tls::client_auth::REQUIRE).then([] {
        BOOST_FAIL("Expected exception");
    }).handle_exception([](auto ep) {
        // ok.
    });
}

SEASTAR_TEST_CASE(test_simple_x509_client_server_client_auth) {
    // Make sure we load our own auth trust pem file, otherwise our certs
    // will not validate
    // Must match expected name with cert CA or give empty name to ignore
    // server name
    // Server will require certificate auth. We supply one, so should succeed with connection
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org", certfile("test.crt"), certfile("test.key"), tls::client_auth::REQUIRE, certfile("test.crt"), certfile("test.key"));
}

SEASTAR_TEST_CASE(test_simple_x509_client_server_client_auth_with_dn_callback) {
    // In addition to the above test, the certificate's subject and issuer
    // Distinguished Names (DNs) will be checked for the occurrence of a specific
    // substring (in this case, the test.scylladb.org url)
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org", certfile("test.crt"), certfile("test.key"), tls::client_auth::REQUIRE, certfile("test.crt"), certfile("test.key"), true, true, [](tls::session_type t, sstring subject, sstring issuer) {
        BOOST_REQUIRE(t == tls::session_type::CLIENT);
        BOOST_REQUIRE(subject.find("test.scylladb.org") != sstring::npos);
        BOOST_REQUIRE(issuer.find("test.scylladb.org") != sstring::npos);
    });
}

SEASTAR_TEST_CASE(test_simple_x509_client_server_client_auth_dn_callback_fails) {
    // Test throwing an exception from within the Distinguished Names callback
    return run_echo_test(message, 20, certfile("catest.pem"), "test.scylladb.org", certfile("test.crt"), certfile("test.key"), tls::client_auth::REQUIRE, certfile("test.crt"), certfile("test.key"), true, true, [](tls::session_type, sstring, sstring) {
        throw tls::verification_error("to test throwing from within the callback");
    }).then([] {
        BOOST_FAIL("Should have gotten a verification_error exception");
    }).handle_exception([](auto) {
        // ok.
    });
}

SEASTAR_TEST_CASE(test_many_large_message_x509_client_server) {
    // Make sure we load our own auth trust pem file, otherwise our certs
    // will not validate
    // Must match expected name with cert CA or give empty name to ignore
    // server name
    sstring msg = uninitialized_string(4 * 1024 * 1024);
    for (size_t i = 0; i < msg.size(); ++i) {
        msg[i] = '0' + char(i % 30);
    }
    // Sending a huge-ish message a and immediately closing the session (see params)
    // provokes case where tls::vec_push entered race and asserted on broken IO state
    // machine.
    auto range = boost::irange(0, 20);
    return do_for_each(range, [msg = std::move(msg)](auto) {
        return run_echo_test(std::move(msg), 1, certfile("catest.pem"), "test.scylladb.org", certfile("test.crt"), certfile("test.key"), tls::client_auth::NONE, {}, {}, false);
    });
}

SEASTAR_THREAD_TEST_CASE(test_close_timout) {
    tls::credentials_builder b;

    b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
    b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
    b.set_dh_level();
    b.set_system_trust().get();

    auto creds = b.build_certificate_credentials();
    auto serv = b.build_server_credentials();

    semaphore sem(0);

    class my_loopback_connected_socket_impl : public loopback_connected_socket_impl {
    public:
        semaphore& _sem;
        bool _close = false;

        my_loopback_connected_socket_impl(semaphore& s, lw_shared_ptr<loopback_buffer> tx, lw_shared_ptr<loopback_buffer> rx)
            : loopback_connected_socket_impl(tx, rx)
            , _sem(s)
        {}
        ~my_loopback_connected_socket_impl() {
            _sem.signal();
        }
        class my_sink_impl : public data_sink_impl {
        public:
            data_sink _sink;
            my_loopback_connected_socket_impl& _impl;
            promise<> _p;
            my_sink_impl(data_sink sink, my_loopback_connected_socket_impl& impl)
                : _sink(std::move(sink))
                , _impl(impl)
            {}
            future<> flush() override {
                return _sink.flush();
            }
            using data_sink_impl::put;
            future<> put(net::packet p) override {
                if (std::exchange(_impl._close, false)) {
                    return _p.get_future().then([this, p = std::move(p)]() mutable {
                        return put(std::move(p));
                    });
                }
                return _sink.put(std::move(p));
            }
            future<> close() override {
                _p.set_value();
                return make_ready_future<>();
            }
        };
        data_sink sink() override {
            return data_sink(std::make_unique<my_sink_impl>(loopback_connected_socket_impl::sink(), *this));
        }
    };

    auto constexpr iterations = 500;
        
    for (int i = 0; i < iterations; ++i) {
        auto b1 = ::make_lw_shared<loopback_buffer>(nullptr, loopback_buffer::type::SERVER_TX);
        auto b2 = ::make_lw_shared<loopback_buffer>(nullptr, loopback_buffer::type::CLIENT_TX);
        auto ssi = std::make_unique<my_loopback_connected_socket_impl>(sem, b1, b2);
        auto csi = std::make_unique<my_loopback_connected_socket_impl>(sem, b2, b1);

        auto& ssir = *ssi;
        auto& csir = *csi;

        auto ss = tls::wrap_server(serv, connected_socket(std::move(ssi))).get();
        auto cs = tls::wrap_client(creds, connected_socket(std::move(csi))).get();

        auto os = cs.output().detach();
        auto is = ss.input();

        auto f1 = os.put(temporary_buffer<char>(10));
        auto f2 = is.read();
        f1.get();
        f2.get();
        
        // block further writes
        ssir._close = true;
        csir._close = true;
    }

    sem.wait(2 * iterations).get();
}

SEASTAR_THREAD_TEST_CASE(test_reload_certificates) {
    tmpdir tmp;

    namespace fs = std::filesystem;

    // copy the wrong certs. We don't trust these
    // blocking calls, but this is a test and seastar does not have a copy 
    // util and I am lazy...
    fs::copy_file(certfile("other.crt"), tmp.path() / "test.crt");
    fs::copy_file(certfile("other.key"), tmp.path() / "test.key");

    auto cert = (tmp.path() / "test.crt").native();
    auto key = (tmp.path() / "test.key").native();
    std::unordered_set<sstring> changed;
    promise<> p;

    tls::credentials_builder b;
    b.set_x509_key_file(cert, key, tls::x509_crt_format::PEM).get();
    b.set_dh_level();

    auto certs = b.build_reloadable_server_credentials([&](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
        if (ep) {
            return;
        }
        changed.insert(files.begin(), files.end());
        if (changed.count(cert) && changed.count(key)) {
            p.set_value();
        }
    }).get();

    ::listen_options opts;
    opts.reuse_address = true;
    auto addr = ::make_ipv4_address( {0x7f000001, 4712});
    auto server = tls::listen(certs, addr, opts);

    tls::credentials_builder b2;
    b2.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();

    {
        auto sa = server.accept();
        auto c = tls::connect(b2.build_certificate_credentials(), addr).get();
        auto s = sa.get();
        auto in = s.connection.input();

        output_stream<char> out(c.output().detach(), 4096);

        try {
            out.write("apa").get();
            auto f = out.flush();
            auto f2 = in.read();

            try {
                f.get();
                BOOST_FAIL("should not reach");
            } catch (tls::verification_error&) {
                // ok
            }
            try {
                out.close().get();
            } catch (...) {
            }

            try {
                f2.get();
                BOOST_FAIL("should not reach");
            } catch (...) {
                // ok
            }
            try {
                in.close().get();
            } catch (...) {
            }
        } catch (tls::verification_error&) {
            // ok
        }
    }

    // copy the right (trusted) certs over the old ones.
    fs::copy_file(certfile("test.crt"), tmp.path() / "test0.crt");
    fs::copy_file(certfile("test.key"), tmp.path() / "test0.key");

    rename_file((tmp.path() / "test0.crt").native(), (tmp.path() / "test.crt").native()).get();
    rename_file((tmp.path() / "test0.key").native(), (tmp.path() / "test.key").native()).get();

    p.get_future().get();

    // now it should work
    {
        auto sa = server.accept();
        auto c = tls::connect(b2.build_certificate_credentials(), addr).get();
        auto s = sa.get();
        auto in = s.connection.input();

        output_stream<char> out(c.output().detach(), 4096);

        out.write("apa").get();
        auto f = out.flush();
        auto buf = in.read().get();
        f.get();
        out.close().get();
        in.read().get(); // ignore - just want eof
        in.close().get();

        BOOST_CHECK_EQUAL(sstring(buf.begin(), buf.end()), "apa");
    }
}

SEASTAR_THREAD_TEST_CASE(test_reload_broken_certificates) {
    tmpdir tmp;

    namespace fs = std::filesystem;

    fs::copy_file(certfile("test.crt"), tmp.path() / "test.crt");
    fs::copy_file(certfile("test.key"), tmp.path() / "test.key");

    auto cert = (tmp.path() / "test.crt").native();
    auto key = (tmp.path() / "test.key").native();
    std::unordered_set<sstring> changed;
    promise<> p;

    tls::credentials_builder b;
    b.set_x509_key_file(cert, key, tls::x509_crt_format::PEM).get();
    b.set_dh_level();

    queue<std::exception_ptr> q(10);

    auto certs = b.build_reloadable_server_credentials([&](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
        if (ep) {
            q.push(std::move(ep));
            return;
        }
        changed.insert(files.begin(), files.end());
        if (changed.count(cert) && changed.count(key)) {
            p.set_value();
        }
    }).get();

    // very intentionally use blocking calls. We want all our modifications to happen
    // before any other continuation is allowed to process.

    fs::remove(cert);
    fs::remove(key);

    std::ofstream(cert.c_str()) << "lala land" << std::endl;
    std::ofstream(key.c_str()) << "lala land" << std::endl;

    // should get one or two exceptions
    q.pop_eventually().get();

    fs::remove(cert);
    fs::remove(key);

    fs::copy_file(certfile("test.crt"), cert);
    fs::copy_file(certfile("test.key"), key);

    // now it should reload
    p.get_future().get();
}

using namespace std::chrono_literals;

// the same as previous test, but we set a big tolerance for 
// reload errors, and verify that either our scheduling/fs is 
// super slow, or we got through the changes without failures.
SEASTAR_THREAD_TEST_CASE(test_reload_tolerance) {
    tmpdir tmp;

    namespace fs = std::filesystem;

    fs::copy_file(certfile("test.crt"), tmp.path() / "test.crt");
    fs::copy_file(certfile("test.key"), tmp.path() / "test.key");

    auto cert = (tmp.path() / "test.crt").native();
    auto key = (tmp.path() / "test.key").native();
    std::unordered_set<sstring> changed;
    promise<> p;

    tls::credentials_builder b;
    b.set_x509_key_file(cert, key, tls::x509_crt_format::PEM).get();
    b.set_dh_level();

    int nfails = 0;

    // use 5s tolerance - this should ensure we don't generate any errors.
    auto certs = b.build_reloadable_server_credentials([&](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
        if (ep) {
            ++nfails;
            return;
        }
        changed.insert(files.begin(), files.end());
        if (changed.count(cert) && changed.count(key)) {
            p.set_value();
        }
    }, std::chrono::milliseconds(5000)).get();

    // very intentionally use blocking calls. We want all our modifications to happen
    // before any other continuation is allowed to process.

    auto start = std::chrono::system_clock::now();

    fs::remove(cert);
    fs::remove(key);

    std::ofstream(cert.c_str()) << "lala land" << std::endl;
    std::ofstream(key.c_str()) << "lala land" << std::endl;

    fs::remove(cert);
    fs::remove(key);

    fs::copy_file(certfile("test.crt"), cert);
    fs::copy_file(certfile("test.key"), key);

    // now it should reload
    p.get_future().get();

    auto end = std::chrono::system_clock::now();

    BOOST_ASSERT(nfails == 0 || (end - start) > 4s);
}

SEASTAR_THREAD_TEST_CASE(test_reload_by_move) {
    tmpdir tmp;
    tmpdir tmp2;

    namespace fs = std::filesystem;

    fs::copy_file(certfile("test.crt"), tmp.path() / "test.crt");
    fs::copy_file(certfile("test.key"), tmp.path() / "test.key");
    fs::copy_file(certfile("test.crt"), tmp2.path() / "test.crt");
    fs::copy_file(certfile("test.key"), tmp2.path() / "test.key");

    auto cert = (tmp.path() / "test.crt").native();
    auto key = (tmp.path() / "test.key").native();
    auto cert2 = (tmp2.path() / "test.crt").native();
    auto key2 = (tmp2.path() / "test.key").native();

    std::unordered_set<sstring> changed;
    promise<> p;

    tls::credentials_builder b;
    b.set_x509_key_file(cert, key, tls::x509_crt_format::PEM).get();
    b.set_dh_level();

    int nfails = 0;

    // use 5s tolerance - this should ensure we don't generate any errors.
    auto certs = b.build_reloadable_server_credentials([&](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
        if (ep) {
            ++nfails;
            return;
        }
        changed.insert(files.begin(), files.end());
        if (changed.count(cert) && changed.count(key)) {
            p.set_value();
        }
    }, std::chrono::milliseconds(5000)).get();

    // very intentionally use blocking calls. We want all our modifications to happen
    // before any other continuation is allowed to process.

    fs::remove(cert);
    fs::remove(key);

    // deletes should _not_ cause errors/reloads
    try {
        with_timeout(std::chrono::steady_clock::now() + 3s, p.get_future()).get();
        BOOST_FAIL("should not reach");
    } catch (timed_out_error&) {
        // ok
    }

    BOOST_REQUIRE_EQUAL(changed.size(), 0);

    p = promise();

    fs::rename(cert2, cert);
    fs::rename(key2, key);

    // now it should reload
    p.get_future().get();

    BOOST_REQUIRE_EQUAL(changed.size(), 2);
    changed.clear();

    // again, without delete

    fs::copy_file(certfile("test.crt"), tmp2.path() / "test.crt");
    fs::copy_file(certfile("test.key"), tmp2.path() / "test.key");

    p = promise();

    fs::rename(cert2, cert);
    fs::rename(key2, key);

    // it should reload here as well.
    p.get_future().get();

    // could get two notifications. but not more. 
    for (int i = 0;; ++i) {
        p = promise();
        try {
            with_timeout(std::chrono::steady_clock::now() + 3s, p.get_future()).get();
            BOOST_ASSERT(i == 0);
        } catch (timed_out_error&) {
            // ok
            break;
        }
    }
}

SEASTAR_THREAD_TEST_CASE(test_closed_write) {
    tls::credentials_builder b;

    b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
    b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
    b.set_dh_level();
    b.set_system_trust().get();
    b.set_client_auth(tls::client_auth::REQUIRE);

    auto creds = b.build_certificate_credentials();
    auto serv = b.build_server_credentials();

    ::listen_options opts;
    opts.reuse_address = true;
    opts.set_fixed_cpu(this_shard_id());

    auto addr = ::make_ipv4_address( {0x7f000001, 4712});
    auto server = tls::listen(serv, addr, opts);

    auto check_same_message_two_writes = [](output_stream<char>& out) {
        std::exception_ptr ep1, ep2;

        try {
            out.write("apa").get();
            out.flush().get();
            BOOST_FAIL("should not reach");
        } catch (...) {
            // ok
            ep1 = std::current_exception();
        }

        try {
            out.write("apa").get();
            out.flush().get();
            BOOST_FAIL("should not reach");
        } catch (...) {
            // ok
            ep2 = std::current_exception();
        }

        try {
            std::rethrow_exception(ep1);
        } catch (std::exception& e1) {
            try {
                std::rethrow_exception(ep2);
            } catch (std::exception& e2) {
                BOOST_REQUIRE_EQUAL(std::string(e1.what()), std::string(e2.what()));
                return;
            }
        }

        BOOST_FAIL("should not reach");
    };


    {
        auto sa = server.accept();
        auto c = tls::connect(creds, addr).get();
        auto s = sa.get();
        auto in = s.connection.input();

        output_stream<char> out(c.output().detach(), 4096);
        // close on client end before writing
        out.close().get();

        check_same_message_two_writes(out);
    }

    {
        auto sa = server.accept();
        auto c = tls::connect(creds, addr).get();
        auto s = sa.get();
        auto in = s.connection.input();

        output_stream<char> out(c.output().detach(), 4096);

        out.write("apa").get();
        auto f = out.flush();
        in.read().get();
        f.get();

        // close on server end before writing
        in.close().get();
        s.connection.shutdown_input();
        s.connection.shutdown_output();

        // we won't get broken pipe until
        // after a while (tm)
        for (;;) {
            try {
                out.write("apa").get();
                out.flush().get();
            } catch (...) {
                break;
            }
        }

        // now check we get the same message.
        check_same_message_two_writes(out);
    }

}

/*
 * Certificates:
 *
 * make -f tests/unit/mkmtls.gmk domain=scylladb.org server=test
 *
 * ->   mtls_ca.crt
 *      mtls_ca.key
 *      mtls_server.crt
 *      mtls_server.csr
 *      mtls_server.key
 *      mtls_client1.crt
 *      mtls_client1.csr
 *      mtls_client1.key
 *      mtls_client2.crt
 *      mtls_client2.csr
 *      mtls_client2.key
 *
 */
SEASTAR_THREAD_TEST_CASE(test_dn_name_handling) {
    // Connect to server using two different client certificates.
    // Make sure that for every client the server can fetch DN string
    // and the string is correct.
    // The setup consist of several certificates:
    // - CA
    // - mtls_server.crt - server certificate
    // - mtls_client1.crt - first client certificate
    // - mtls_client2.crt - second client certificate
    //
    // The test runs server that uses mtls_server.crt.
    // The server accepts two incomming connections, first one uses mtls_client1.crt
    // and the second one uses mtls_client2.crt. Every client sends a short string
    // that server receives and tries to find it in the DN string.

    auto addr = ::make_ipv4_address( {0x7f000001, 4712});

    auto client1_creds = [] {
        tls::credentials_builder builder;
        builder.set_x509_trust_file(certfile("mtls_ca.crt"), tls::x509_crt_format::PEM).get();
        builder.set_x509_key_file(certfile("mtls_client1.crt"), certfile("mtls_client1.key"), tls::x509_crt_format::PEM).get();
        return builder.build_certificate_credentials();
    }();

    auto client2_creds = [] {
        tls::credentials_builder builder;
        builder.set_x509_trust_file(certfile("mtls_ca.crt"), tls::x509_crt_format::PEM).get();
        builder.set_x509_key_file(certfile("mtls_client2.crt"), certfile("mtls_client2.key"), tls::x509_crt_format::PEM).get();
        return builder.build_certificate_credentials();
    }();

    auto server_creds = [] {
        tls::credentials_builder builder;
        builder.set_x509_trust_file(certfile("mtls_ca.crt"), tls::x509_crt_format::PEM).get();
        builder.set_x509_key_file(certfile("mtls_server.crt"), certfile("mtls_server.key"), tls::x509_crt_format::PEM).get();
        builder.set_client_auth(tls::client_auth::REQUIRE);
        return builder.build_server_credentials();
    }();

    auto fetch_dn = [server_creds, addr] (sstring id, shared_ptr<tls::certificate_credentials> client_cred) {
        listen_options lo{};
        lo.reuse_address = true;
        auto server_sock = tls::listen(server_creds, addr, lo);

        auto sa = server_sock.accept();
        auto c = tls::connect(client_cred, addr).get();
        auto s = sa.get();

        auto in = s.connection.input();
        output_stream<char> out(c.output().detach(), 1024);
        out.write(id).get();

        auto fdn = tls::get_dn_information(s.connection);

        auto fout = out.flush();
        auto fin = in.read();

        fout.get();

        auto dn = fdn.get();
        auto client_id = fin.get();

        in.close().get();
        out.close().get();

        s.connection.shutdown_input();
        s.connection.shutdown_output();

        c.shutdown_input();
        c.shutdown_output();

        auto it = dn->subject.find(sstring(client_id.get(), client_id.size()));
        BOOST_REQUIRE(it != sstring::npos);
    };

    fetch_dn("client1.org", client1_creds);
    fetch_dn("client2.org", client2_creds);
}

SEASTAR_THREAD_TEST_CASE(test_alt_names) {
    tls::credentials_builder b;

    b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
    b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
    b.set_client_auth(tls::client_auth::REQUIRE);

    auto creds = b.build_certificate_credentials();
    auto serv = b.build_server_credentials();

    ::listen_options opts;
    opts.reuse_address = true;
    opts.set_fixed_cpu(this_shard_id());

    auto addr = ::make_ipv4_address( {0x7f000001, 4712});
    auto server = tls::listen(serv, addr, opts);

    {
        auto sa = server.accept();
        auto c = tls::connect(creds, addr).get();
        auto s = sa.get();

        auto in = s.connection.input();
        output_stream<char> out(c.output().detach(), 1024);
        out.write("nils").get();

        auto falt_names = tls::get_alt_name_information(s.connection);

        auto fout = out.flush();
        auto fin = in.read();

        fout.get();

        auto alt_names = falt_names.get();
        fin.get();

        in.close().get();
        out.close().get();

        s.connection.shutdown_input();
        s.connection.shutdown_output();

        c.shutdown_input();
        c.shutdown_output();

        auto ensure_alt_name = [&](tls::subject_alt_name_type type, size_t min_count) {
            for (auto& v : alt_names) {
                if (type != v.type) {
                    continue;
                }
                std::visit([&](auto& val) {
                    BOOST_TEST_MESSAGE(fmt::format("Alt name type: {}: {}", int(type), val).c_str());
                }, v.value);
                if (--min_count == 0) {
                    return;
                }
            }
            BOOST_FAIL("Missing " + std::to_string(min_count) + " alt name attributes of type " + std::to_string(int(type)));
        };

        ensure_alt_name(tls::subject_alt_name_type::ipaddress, 1);
        ensure_alt_name(tls::subject_alt_name_type::rfc822name, 2);
        ensure_alt_name(tls::subject_alt_name_type::dnsname, 1);
    }

}

SEASTAR_THREAD_TEST_CASE(test_skip_wait_for_eof) {
    tls::credentials_builder b;

    b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
    b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
    b.set_client_auth(tls::client_auth::REQUIRE);

    auto creds = b.build_certificate_credentials();
    auto serv = b.build_server_credentials();

    ::listen_options opts;
    opts.reuse_address = true;
    opts.set_fixed_cpu(this_shard_id());

    auto addr = ::make_ipv4_address({0x7f000001, 4712});
    auto server = tls::listen(serv, addr, opts);

    {
        // Initiate a connection while specifying that it should not wait for eof on shutdown.
        auto sa = server.accept();
        auto c = engine().connect(addr).get();
        auto c_tls = tls::wrap_client(creds, std::move(c),
                                      tls::tls_options{.wait_for_eof_on_shutdown = false}).get();
        auto s = sa.get();

        auto in = s.connection.input();
        auto out = c_tls.output();

        // Write some data in the socket to handshake.
        out.write("apa").get();
        auto f = out.flush();
        auto buf = in.read().get();
        f.get();
        BOOST_CHECK(sstring(buf.begin(), buf.end()) == "apa");

        // Prevent the server from reading from the connection.
        // This ensures that it will miss the bye message and not
        // reply with an eof.
        server.abort_accept();

        // Initiate closing of the TLS session
        c_tls.shutdown_input();
        c_tls.shutdown_output();

        // Ensure that the session is closed promptly. When wait_for_eof_on_shutdown is not
        // specified, the call to wait_input_shutdown will hang for 10 seconds waiting for
        // and eof from the server.
        try {
            with_timeout(std::chrono::steady_clock::now() + 1s, c_tls.wait_input_shutdown()).get();
        } catch (timed_out_error&) {
            BOOST_FAIL("Timed out while waiting for input shutdown."
                       "This indicates the EOF wait was not skipped");
        }
    }
}

/**
 * Test TLS13 session ticket support. 
*/
SEASTAR_THREAD_TEST_CASE(test_tls13_session_tickets) {
    tls::credentials_builder b;

    b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get();
    b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get();
    b.set_session_resume_mode(tls::session_resume_mode::TLS13_SESSION_TICKET);
    b.set_priority_string("SECURE128:+SECURE192:-VERS-TLS-ALL:+VERS-TLS1.3");

    auto creds = b.build_certificate_credentials();
    auto serv = b.build_server_credentials();

    ::listen_options opts;
    opts.reuse_address = true;
    opts.set_fixed_cpu(this_shard_id());

    auto addr = ::make_ipv4_address( {0x7f000001, 4712});
    auto server = tls::listen(serv, addr, opts);

    tls::session_data sess_data;

    {
        auto sa = server.accept();
        auto c = tls::connect(creds, addr).get();
        auto s = sa.get();

        auto in = s.connection.input();
        auto cin = c.input();
        output_stream<char> out(c.output().detach(), 1024);
        output_stream<char> sout(s.connection.output().detach(), 1024);

        // write data in both directions. Required for session data to 
        // become available. 
        out.write("nils").get();
        auto fin = in.read();
        auto fout = out.flush();

        fout.get();
        fin.get();

        sout.write("banan").get();
        fin = cin.read();
        fout = sout.flush();

        fout.get();
        fin.get();

        BOOST_REQUIRE(!tls::check_session_is_resumed(c).get()); // no resume data

        // get ticket data
        sess_data = tls::get_session_resume_data(c).get();
        BOOST_REQUIRE(!sess_data.empty());

        in.close().get();
        out.close().get();

        s.connection.shutdown_input();
        s.connection.shutdown_output();

        c.shutdown_input();
        c.shutdown_output();
    }


    {
        auto sa = server.accept();

        // tell client to try resuming.
        tls::tls_options tls_opts;
        tls_opts.session_resume_data = sess_data;

        auto c = tls::connect(creds, addr, tls_opts).get();
        auto s = sa.get();

        // This is ok. Will force a handshake.
        auto f = tls::check_session_is_resumed(c);

        // But we need to force some IO to make the 
        // handshake actually happen.
        auto in = s.connection.input();
        output_stream<char> out(c.output().detach(), 1024);

        auto fin = in.read();

        out.write("nils").get();
        auto fout = out.flush();

        fout.get();
        fin.get();

        BOOST_REQUIRE(f.get()); // Should work

        in.close().get();
        out.close().get();

        s.connection.shutdown_input();
        s.connection.shutdown_output();

        c.shutdown_input();
        c.shutdown_output();
    }

}
