Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions src/mgmt/rpc/server/unit_tests/test_rpcserver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <unistd.h>

#include <thread>
#include <future>
#include <chrono>
#include <fstream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <vector>

#include "swoc/swoc_file.h"

Expand Down Expand Up @@ -71,12 +76,75 @@ add_method_handler(const std::string &name, Func &&call)

namespace
{
const std::string sockPath{"tests/var/jsonrpc20_test.sock"};
const std::string lockPath{"tests/var/jsonrpc20_test.lock"};
constexpr int default_backlog{5};
constexpr int default_maxRetriesOnTransientErrors{64};
constexpr size_t default_incoming_req_max_size{32000 * 3};
DbgCtl dbg_ctl{"rpc.test.client"};
constexpr std::string_view rpc_test_dir_template{"ats_rpc_XXXXXX"};
constexpr std::string_view rpc_test_socket_name{"s"};
constexpr std::string_view rpc_test_lock_name{"l"};
constexpr size_t max_rpc_socket_path_size{sizeof(sockaddr_un::sun_path) - 1};

fs::path rpcTestDir;
std::string sockPath;
std::string lockPath;
constexpr int default_backlog{5};
constexpr int default_maxRetriesOnTransientErrors{64};
constexpr size_t default_incoming_req_max_size{32000 * 3};
DbgCtl dbg_ctl{"rpc.test.client"};

/** Prepare JSONRPC socket paths beneath @a base.
*
* This owns creation of the per-run test directory and publishes the socket
* and lock paths shared by the JSONRPC test server and clients.
*
* @param[in] base Candidate parent directory for the test directory.
* @param[out] error Reason setup failed, if a usable directory was not created.
* @return @c true if the socket and lock paths are ready for this test run.
*/
bool
try_setup_rpc_test_paths(fs::path const &base, std::string &error)
{
auto const dir_template = (base / rpc_test_dir_template).string();
auto const socket_path = (fs::path{dir_template} / rpc_test_socket_name).string();

if (socket_path.size() > max_rpc_socket_path_size) {
error = "JSONRPC test socket path is too long under " + base.string() + ": " + socket_path;
return false;
}

std::vector<char> mutable_template{dir_template.begin(), dir_template.end()};
mutable_template.push_back('\0');

char *created_dir = mkdtemp(mutable_template.data());
if (created_dir == nullptr) {
error = "Failed to create JSONRPC test directory under " + base.string() + ": " + std::strerror(errno);
return false;
}

rpcTestDir = fs::path{created_dir};
sockPath = (rpcTestDir / rpc_test_socket_name).string();
lockPath = (rpcTestDir / rpc_test_lock_name).string();
return true;
}

/** Prepare JSONRPC socket paths for the test run.
*
* This prefers the environment temporary directory, then falls back to @c /tmp
* when the generated Unix-domain socket path would be too long or setup fails.
*
* @param[out] error Reason setup failed, if no candidate directory works.
* @return @c true if the socket and lock paths are ready for this test run.
*/
bool
setup_rpc_test_paths(std::string &error)
{
if (try_setup_rpc_test_paths(fs::temp_directory_path(), error)) {
error.clear();
return true;
}
if (try_setup_rpc_test_paths(fs::path{"/tmp"}, error)) {
error.clear();
return true;
}
Comment thread
bneradt marked this conversation as resolved.
return false;
}

} // end anonymous namespace

Expand All @@ -88,6 +156,11 @@ struct RPCServerTestListener : Catch::EventListenerBase {
void
testRunStarting(Catch::TestRunInfo const & /* testRunInfo ATS_UNUSED */) override
{
std::string setup_error;
bool const setup_ok = setup_rpc_test_paths(setup_error);
INFO(setup_error);
REQUIRE(setup_ok);

Layout::create();
init_diags("rpc", nullptr);
RecProcessInit();
Expand Down Expand Up @@ -123,6 +196,11 @@ struct RPCServerTestListener : Catch::EventListenerBase {
if (jsonrpcServer) {
delete jsonrpcServer; // will stop the thread
}

std::error_code ec;
if (!rpcTestDir.empty()) {
fs::remove_all(rpcTestDir, ec);
}
}

private:
Expand Down
222 changes: 158 additions & 64 deletions tests/gold_tests/autest-site/conditions.test.ext
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,107 @@ import os
import subprocess
import json
import re
import tempfile
import time

from ports import get_port_number

OPENSSL_TLS_FLAGS = {
"1.0": "-tls1",
"1.1": "-tls1_1",
"1.2": "-tls1_2",
"1.3": "-tls1_3",
}


def _terminate_process(process):
if process.poll() is not None:
return
process.terminate()
try:
process.wait(timeout=2)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=2)
Comment on lines +43 to +44


def _probe_openssl_server(tls_version, client_probe):
"""Run a local OpenSSL server for TLS capability probes.

This owns the temporary certificate, port allocation, and server process
lifecycle so AuTest conditions can perform a real handshake with the client
being checked. Local setup failures return ``False`` so callers can skip
dependent tests instead of failing the harness.

:param tls_version: TLS version string to look up in
``OPENSSL_TLS_FLAGS``.
:param client_probe: Callable that receives the server port and TLS flag
and returns whether the client completed the expected handshake.
:returns: ``True`` if the client probe succeeds against the local server,
otherwise ``False``.
"""
tls_flag = OPENSSL_TLS_FLAGS.get(tls_version)
if tls_flag is None:
return False

try:
with tempfile.TemporaryDirectory() as tmpdir:
cert_path = os.path.join(tmpdir, "cert.pem")
key_path = os.path.join(tmpdir, "key.pem")
result = subprocess.run(
[
"openssl",
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-sha256",
"-keyout",
key_path,
"-out",
cert_path,
"-subj",
"/CN=localhost",
"-days",
"1",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=10,
)
if result.returncode != 0:
return False

port = get_port_number()
server = subprocess.Popen(
[
"openssl",
"s_server",
Comment on lines +94 to +99
"-quiet",
"-accept",
f"127.0.0.1:{port}",
"-cert",
cert_path,
"-key",
key_path,
tls_flag,
"-cipher",
"DEFAULT@SECLEVEL=0",
"-www",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
time.sleep(0.5)
if server.poll() is not None:
return False
return client_probe(port, tls_flag)
finally:
_terminate_process(server)
except (OSError, subprocess.SubprocessError):
return False


def HasOpenSSLVersion(self, version):
Expand Down Expand Up @@ -67,38 +168,39 @@ def HasLegacyTLSSupport(self):
always disable both legacy versions together. If TLSv1.0 is unavailable,
TLSv1.1 will be too.

The check connects to localhost on a closed port to avoid any external
network dependency. A "connection refused" error means the TLS protocol
was available but nothing was listening; "no protocols available" means
the crypto-policy blocked TLSv1.0 entirely.
The check starts a local OpenSSL server and uses an OpenSSL client to
complete a real TLSv1.0 handshake. This avoids external network dependency
while catching environments that accept legacy TLS command-line flags but
reject the protocol during an actual handshake.
"""

def check_tls1_support():
try:
# Connect to localhost on a port nothing is listening on.
# This avoids external network dependency while still detecting
# whether the crypto-policy allows TLSv1.0.
result = subprocess.run(
['openssl', 's_client', '-tls1', '-connect', '127.0.0.1:1'],
capture_output=True,
text=True,
timeout=5,
input='' # Don't wait for interactive input
)
output = result.stdout + result.stderr
# "no protocols available" means TLSv1 is disabled by crypto-policy
if 'no protocols available' in output:

def client_probe(port, tls_flag):
Comment thread
bneradt marked this conversation as resolved.
try:
result = subprocess.run(
[
'openssl',
's_client',
tls_flag,
'-connect',
f'127.0.0.1:{port}',
'-cipher',
'DEFAULT@SECLEVEL=0',
'-brief',
],
capture_output=True,
text=True,
timeout=5,
input='Q\n')
output = result.stdout + result.stderr
return result.returncode == 0 and 'Protocol version: TLSv1' in output
except subprocess.TimeoutExpired:
return False
# Connection refused or other errors mean TLSv1 was attempted
# (the protocol is available, just no server listening)
return True
except subprocess.TimeoutExpired:
# Timeout on localhost shouldn't happen, but if it does,
# assume TLSv1 is not available (safer than false positive)
return False
except Exception:
# If we can't determine, assume TLSv1 is not available (safer)
return False
except Exception:
return False

return _probe_openssl_server("1.0", client_probe)

return self.Condition(check_tls1_support, "System does not support legacy TLS protocols (TLSv1.0/TLSv1.1)")

Expand Down Expand Up @@ -154,7 +256,6 @@ def HasCurlTLSVersionSupport(self, tls_version):
"""

def check_curl_tls_support():
# Map semantic versions used by tests to curl flags.
version_map = {
"1.0": ("--tlsv1", "1.0"),
"1.1": ("--tlsv1.1", "1.1"),
Expand All @@ -165,43 +266,36 @@ def HasCurlTLSVersionSupport(self, tls_version):
return False

tls_flag, tls_max = version_map[tls_version]
try:
# Connect to localhost closed port to avoid network dependencies.
# "connection refused" means curl accepted the TLS flags and tried.
result = subprocess.run(
[
"curl",
"-svk",
"--connect-timeout",
"2",
"--max-time",
"3",
tls_flag,
"--tls-max",
tls_max,
"https://127.0.0.1:1",
],
capture_output=True,
text=True,
timeout=5,
)
output = (result.stdout + result.stderr).lower()
unsupported_markers = [
"unsupported protocol",
"no protocols available",
"option --tlsv",
"unknown option",
"is unknown",
]
if any(marker in output for marker in unsupported_markers):

def client_probe(port, _tls_flag):
try:
result = subprocess.run(
[
"curl",
"-svk",
"--connect-timeout",
"2",
"--max-time",
"5",
"--ciphers",
"DEFAULT@SECLEVEL=0",
tls_flag,
"--tls-max",
tls_max,
f"https://127.0.0.1:{port}/",
],
capture_output=True,
text=True,
timeout=7,
)
output = (result.stdout + result.stderr).lower()
return result.returncode == 0 and f"ssl connection using tlsv{tls_version}" in output
except subprocess.TimeoutExpired:
return False
except Exception:
return False

# Any attempt to connect implies curl accepted the TLS setting.
return True
except subprocess.TimeoutExpired:
return False
except Exception:
return False
return _probe_openssl_server(tls_version, client_probe)

return self.Condition(
check_curl_tls_support, "Curl does not support TLSv{version} in this environment".format(version=tls_version))
Expand Down
Loading