mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-11-03 02:37:07 +09:00
161 lines
5.7 KiB
C++
161 lines
5.7 KiB
C++
/**
|
|
* @file tests/unit/test_nvhttp_https.cpp
|
|
* @brief Verify HTTPS callbacks retain the guard protecting server lifetime.
|
|
*/
|
|
|
|
// test includes
|
|
#include "../tests_common.h"
|
|
|
|
// standard includes
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <future>
|
|
#include <thread>
|
|
|
|
// local includes
|
|
#include <src/crypto.h>
|
|
#include <src/nvhttp.h>
|
|
|
|
namespace {
|
|
using namespace std::chrono_literals;
|
|
using boost::asio::ip::tcp;
|
|
|
|
/**
|
|
* @brief Run real HTTPS callbacks against an isolated loopback listener.
|
|
*/
|
|
struct HttpsHandlerLifetimeTest: BaseTest {
|
|
/**
|
|
* @brief Create credentials and start the production HTTPS accept handler.
|
|
*/
|
|
void SetUp() override {
|
|
BaseTest::SetUp();
|
|
const auto credentials = crypto::gen_creds("Sunshine HTTPS lifetime test", 2048);
|
|
certificate_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_cert.pem";
|
|
private_key_file = std::filesystem::path {SUNSHINE_TEST_BIN_DIR} / "https_lifetime_key.pem";
|
|
std::ofstream {certificate_file} << credentials.x509;
|
|
std::ofstream {private_key_file} << credentials.pkey;
|
|
|
|
server = nvhttp::test_support::make_https_server(certificate_file.string(), private_key_file.string(), handler_scope);
|
|
server->io_service = std::make_shared<boost::asio::io_context>();
|
|
server->config.address = "127.0.0.1";
|
|
server->config.port = 0;
|
|
server->start([this](unsigned short assigned_port) {
|
|
port = assigned_port;
|
|
});
|
|
server->io_service->poll();
|
|
ASSERT_NE(port, 0);
|
|
}
|
|
|
|
/**
|
|
* @brief Drain canceled callbacks and remove the generated credentials.
|
|
*/
|
|
void TearDown() override {
|
|
if (server) {
|
|
server->stop();
|
|
server->io_service->restart();
|
|
server->io_service->poll();
|
|
server.reset();
|
|
}
|
|
if (stop_thread.joinable()) {
|
|
stop_thread.join();
|
|
}
|
|
std::error_code cleanup_error;
|
|
std::filesystem::remove(certificate_file, cleanup_error);
|
|
std::filesystem::remove(private_key_file, cleanup_error);
|
|
BaseTest::TearDown();
|
|
}
|
|
|
|
/**
|
|
* @brief Check that stopping the scope runner waits until the error callback exits.
|
|
*
|
|
* @param callback_error Error delivered by the real accept or TLS handshake operation.
|
|
*/
|
|
void check_handler_guard(const boost::system::error_code &callback_error) {
|
|
EXPECT_TRUE(callback_error);
|
|
++error_callbacks;
|
|
std::promise<void> stopping;
|
|
auto stopping_future = stopping.get_future();
|
|
std::promise<void> stopped;
|
|
stopped_future = stopped.get_future();
|
|
stop_thread = std::jthread([this, stopping = std::move(stopping), stopped = std::move(stopped)]() mutable {
|
|
stopping.set_value();
|
|
handler_scope->stop();
|
|
stopped.set_value();
|
|
});
|
|
stopping_future.wait();
|
|
EXPECT_EQ(stopped_future.wait_for(50ms), std::future_status::timeout);
|
|
}
|
|
|
|
std::unique_ptr<SimpleWeb::ServerBase<nvhttp::SunshineHTTPS>> server; ///< Server running production accept and handshake callbacks.
|
|
std::shared_ptr<SimpleWeb::ScopeRunner> handler_scope; ///< Guard used by the server destructor.
|
|
std::jthread stop_thread; ///< Concurrent attempt to stop the handler scope.
|
|
std::future<void> stopped_future; ///< Signals that all callback guards have been released.
|
|
unsigned short port = 0; ///< Assigned loopback port.
|
|
int error_callbacks = 0; ///< Number of errors delivered while testing a handler.
|
|
std::filesystem::path certificate_file; ///< Generated server certificate.
|
|
std::filesystem::path private_key_file; ///< Generated server private key.
|
|
};
|
|
|
|
/**
|
|
* @brief Canceling a pending accept holds the server guard through its error callback.
|
|
*/
|
|
TEST_F(HttpsHandlerLifetimeTest, AcceptErrorRetainsGuardUntilCallbackReturns) {
|
|
server->on_error = [this](auto, const auto &callback_error) {
|
|
check_handler_guard(callback_error);
|
|
};
|
|
server->stop();
|
|
server->io_service->run_for(2s);
|
|
ASSERT_EQ(error_callbacks, 1);
|
|
ASSERT_TRUE(stopped_future.valid());
|
|
EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready);
|
|
}
|
|
|
|
/**
|
|
* @brief A failed TLS handshake holds the server guard through its error callback.
|
|
*/
|
|
TEST_F(HttpsHandlerLifetimeTest, HandshakeErrorRetainsGuardUntilCallbackReturns) {
|
|
server->on_error = [this](auto, const auto &callback_error) {
|
|
check_handler_guard(callback_error);
|
|
};
|
|
boost::asio::io_context client_io;
|
|
tcp::socket client {client_io};
|
|
client.connect({boost::asio::ip::address_v4::loopback(), port});
|
|
client.close();
|
|
server->io_service->run_for(2s);
|
|
ASSERT_EQ(error_callbacks, 1);
|
|
ASSERT_TRUE(stopped_future.valid());
|
|
EXPECT_EQ(stopped_future.wait_for(2s), std::future_status::ready);
|
|
}
|
|
|
|
/**
|
|
* @brief A stopped server scope prevents a canceled accept from dispatching an error.
|
|
*/
|
|
TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsAcceptCallback) {
|
|
server->on_error = [this](auto, const auto &) {
|
|
++error_callbacks;
|
|
};
|
|
handler_scope->stop();
|
|
server->stop();
|
|
server->io_service->run_for(2s);
|
|
EXPECT_EQ(error_callbacks, 0);
|
|
}
|
|
|
|
/**
|
|
* @brief A stopped server scope prevents an outstanding handshake from dispatching an error.
|
|
*/
|
|
TEST_F(HttpsHandlerLifetimeTest, StoppedScopeSkipsHandshakeCallback) {
|
|
server->on_error = [this](auto, const auto &) {
|
|
++error_callbacks;
|
|
};
|
|
boost::asio::io_context client_io;
|
|
tcp::socket client {client_io};
|
|
client.connect({boost::asio::ip::address_v4::loopback(), port});
|
|
server->io_service->poll();
|
|
handler_scope->stop();
|
|
client.close();
|
|
server->io_service->run_for(2s);
|
|
EXPECT_EQ(error_callbacks, 0);
|
|
}
|
|
} // namespace
|