Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Redis SSL support #902

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
run: |
sudo apt-get update

packages="elfutils libhiredis-dev libzstd-dev ninja-build pkg-config python3 redis-server redis-tools"
packages="elfutils libhiredis-dev libzstd-dev ninja-build pkg-config python3 redis-server redis-tools openssl"
# Install ld.gold (binutils) and ld.lld (lld) on different runs.
if [ "${{ matrix.os }}" = "ubuntu-22.04" ]; then
sudo apt-get install -y $packages binutils
Expand Down
9 changes: 9 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ if(REDIS_STORAGE_BACKEND)
find_package(hiredis 0.13.3 MODULE REQUIRED)
endif()

option(REDISS_STORAGE_BACKEND "Enable Redis SSL remote storage" OFF)
if(REDISS_STORAGE_BACKEND)
message(WARNING
"Linking against OpenSSL doubles ccache startup time. Distributions are "
"encouraged to provide two ccache binaries: one without any extra "
"backends and one with the all the desired backends enabled.")
find_package(hiredis 1.0.0 REQUIRED)
endif()

#
# Special flags
#
Expand Down
48 changes: 48 additions & 0 deletions cmake/Findhiredis.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ if(POLICY CMP0135)
endif()

set(hiredis_FOUND FALSE)
set(hiredis_ssl_FOUND FALSE)

if(HIREDIS_FROM_INTERNET AND NOT HIREDIS_FROM_INTERNET STREQUAL "AUTO")
message(STATUS "Using hiredis from the Internet")
Expand All @@ -22,6 +23,13 @@ else()
message(STATUS "Using hiredis from ${HIREDIS_LIBRARY} via pkg-config")
set(hiredis_FOUND TRUE)
endif()
pkg_check_modules(HIREDIS_SSL hiredis_ssl>=${hiredis_FIND_VERSION})
find_library(HIREDIS_SSL_LIBRARY ${HIREDIS_SSL_LIBRARIES} HINTS ${HIREDIS_SSL_LIBDIR})
find_path(HIREDIS_SSL_INCLUDE_DIR hiredis/hiredis_ssl.h HINTS ${HIREDIS_SSL_PREFIX}/include)
if(HIREDIS_SSL_LIBRARY AND HIREDIS_SSL_INCLUDE_DIR)
message(STATUS "Using hiredis_ssl from ${HIREDIS_SSL_LIBRARY} via pkg-config")
set(hiredis_ssl_FOUND TRUE)
endif()
endif()

if(NOT hiredis_FOUND)
Expand All @@ -31,6 +39,12 @@ else()
message(STATUS "Using hiredis from ${HIREDIS_LIBRARY}")
set(hiredis_FOUND TRUE)
endif()
find_library(HIREDIS_SSL_LIBRARY hiredis_ssl)
find_path(HIREDIS_SSL_INCLUDE_DIR hiredis/hiredis_ssl.h)
if(HIREDIS_SSL_LIBRARY AND HIREDIS_SSL_INCLUDE_DIR)
message(STATUS "Using hiredis_ssl from ${HIREDIS_SSL_LIBRARY} via pkg-config")
set(hiredis_ssl_FOUND TRUE)
endif()
endif()

if(hiredis_FOUND)
Expand Down Expand Up @@ -78,6 +92,8 @@ if(do_download)
FetchContent_Populate(hiredis)
endif()

find_package(OpenSSL)

set(
hiredis_sources
"${hiredis_dir}/alloc.c"
Expand All @@ -89,8 +105,14 @@ if(do_download)
"${hiredis_dir}/sds.c"
"${hiredis_dir}/sockcompat.c"
)
set(
hiredis_ssl_sources
"${hiredis_dir}/ssl.c"
)
add_library(libhiredis_static STATIC EXCLUDE_FROM_ALL ${hiredis_sources})
add_library(HIREDIS::HIREDIS ALIAS libhiredis_static)
add_library(libhiredis_ssl_static STATIC EXCLUDE_FROM_ALL ${hiredis_ssl_sources})
add_library(HIREDIS::HIREDIS_SSL ALIAS libhiredis_ssl_static)

if(WIN32)
target_compile_definitions(libhiredis_static PRIVATE _CRT_SECURE_NO_WARNINGS)
Expand All @@ -104,11 +126,37 @@ if(do_download)
libhiredis_static
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${hiredis_dir}/include>")
set_target_properties(
libhiredis_ssl_static
PROPERTIES
INTERFACE_LINK_LIBRARIES OpenSSL::SSL
INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${hiredis_dir}/include>")

set(hiredis_FOUND TRUE)
set(hiredis_ssl_FOUND TRUE)

set(target libhiredis_static)
endif()

if(HIREDIS_SSL_INCLUDE_DIR AND HIREDIS_SSL_LIBRARY)
mark_as_advanced(HIREDIS_SSL_INCLUDE_DIR HIREDIS_SSL_LIBRARY)

add_library(HIREDIS::HIREDIS_SSL UNKNOWN IMPORTED)
set_target_properties(
HIREDIS::HIREDIS_SSL
PROPERTIES
IMPORTED_LOCATION "${HIREDIS_SSL_LIBRARY}"
INTERFACE_COMPILE_OPTIONS "${HIREDIS_SSL_CFLAGS_OTHER}"
INTERFACE_INCLUDE_DIRECTORIES "${HIREDIS_SSL_INCLUDE_DIR}")

set(hiredis_ssl_FOUND TRUE)
elseif(HIREDIS_FROM_INTERNET)
elseif(REDISS_STORAGE_BACKEND)
message(STATUS
"please install libhiredis_ssl or use -DHIREDIS_FROM_INTERNET=ON or disable with -DREDISS_STORAGE_BACKEND=OFF"
)
endif()

if(WIN32 AND hiredis_FOUND)
target_link_libraries(${target} INTERFACE ws2_32)
endif()
Expand Down
6 changes: 5 additions & 1 deletion doc/MANUAL.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ The default is *subdirs*.

URL formats:

`+redis://[[USERNAME:]PASSWORD@]HOST[:PORT][/DBNUMBER]+` +
`+redis[s]://[[USERNAME:]PASSWORD@]HOST[:PORT][/DBNUMBER]+`
`+redis+unix:SOCKET_PATH[?db=DBNUMBER]+` +
`+redis+unix://[[USERNAME:]PASSWORD@localhost]SOCKET_PATH[?db=DBNUMBER]+`

Expand All @@ -1306,9 +1306,13 @@ Examples:
* `+redis+unix:/run/redis.sock+`
* `+redis+unix:///run/redis.sock+`
* `+redis+unix://p4ssw0rd@localhost/run/redis.sock?db=0+`
* `+rediss://localhost+`

Optional attributes:

* *cacert*: Path to a SSL Root CA file.
* *cert*: Path to a SSL certificate file.
* *key*: Path to a SSL private key file.
* *connect-timeout*: Timeout (in ms) for network connection. The default is 100.
* *operation-timeout*: Timeout (in ms) for Redis commands. The default is 10000.

Expand Down
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ if(REDIS_STORAGE_BACKEND)
PUBLIC standard_settings standard_warnings HIREDIS::HIREDIS third_party
)
endif()
if(REDISS_STORAGE_BACKEND)
target_compile_definitions(ccache_framework PRIVATE -DHAVE_REDISS_STORAGE_BACKEND)
target_link_libraries(ccache_framework PUBLIC HIREDIS::HIREDIS HIREDIS::HIREDIS_SSL)
endif()

add_executable(test-lockfile test_lockfile.cpp)
target_link_libraries(test-lockfile PRIVATE standard_settings standard_warnings ccache_framework)
Expand Down
5 changes: 4 additions & 1 deletion src/storage/Storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#include <util/assertions.hpp>
#include <util/fmtmacros.hpp>
#include <util/logging.hpp>
#ifdef HAVE_REDIS_STORAGE_BACKEND
#if defined(HAVE_REDIS_STORAGE_BACKEND) || defined(HAVE_REDISS_STORAGE_BACKEND)
# include <storage/remote/RedisStorage.hpp>
#endif
#include <util/Bytes.hpp>
Expand Down Expand Up @@ -59,6 +59,9 @@ const std::unordered_map<std::string /*scheme*/,
{"redis", std::make_shared<remote::RedisStorage>()},
{"redis+unix", std::make_shared<remote::RedisStorage>()},
#endif
#ifdef HAVE_REDISS_STORAGE_BACKEND
{"rediss", std::make_shared<remote::RedisStorage>()},
#endif
};

std::string
Expand Down
2 changes: 1 addition & 1 deletion src/storage/remote/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set(
RemoteStorage.cpp
)

if(REDIS_STORAGE_BACKEND)
if(REDIS_STORAGE_BACKEND OR REDISS_STORAGE_BACKEND)
list(APPEND sources RedisStorage.cpp)
endif()

Expand Down
92 changes: 91 additions & 1 deletion src/storage/remote/RedisStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
# pragma warning(disable : 4200)
#endif
#include <hiredis/hiredis.h>
#ifdef HAVE_REDISS_STORAGE_BACKEND
# include <hiredis/hiredis_ssl.h>
#endif
#ifdef _MSC_VER
# pragma warning(pop)
#endif
Expand All @@ -58,6 +61,10 @@ namespace storage::remote {
namespace {

using RedisContext = std::unique_ptr<redisContext, decltype(&redisFree)>;
#ifdef HAVE_REDISS_STORAGE_BACKEND
using RedisSSLContext =
std::unique_ptr<redisSSLContext, decltype(&redisFreeSSLContext)>;
#endif
using RedisReply = std::unique_ptr<redisReply, decltype(&freeReplyObject)>;

const uint32_t DEFAULT_PORT = 6379;
Expand All @@ -79,10 +86,22 @@ class RedisStorageBackend : public RemoteStorage::Backend

private:
const std::string m_prefix;
#ifdef HAVE_REDISS_STORAGE_BACKEND
RedisSSLContext m_ssl_context;
#endif
RedisContext m_context;

#ifdef HAVE_REDISS_STORAGE_BACKEND
void init_ssl(const Url& url,
std::optional<std::string> ca_cert,
std::optional<std::string> cert,
std::optional<std::string> key);
#endif
void
connect(const Url& url, uint32_t connect_timeout, uint32_t operation_timeout);
#ifdef HAVE_REDISS_STORAGE_BACKEND
void initiate_ssl(const Url& url);
#endif
void select_database(const Url& url);
void authenticate(const Url& url);
tl::expected<RedisReply, Failure> redis_command(const char* format, ...);
Expand Down Expand Up @@ -114,13 +133,29 @@ split_user_info(const std::string& user_info)
}
}

#ifdef HAVE_REDISS_STORAGE_BACKEND
inline bool
is_secure(const Url& url)
{
return url.scheme() == "rediss";
}
#endif

RedisStorageBackend::RedisStorageBackend(
const Url& url,
const std::vector<Backend::Attribute>& attributes)
: m_prefix("ccache"), // TODO: attribute
#ifdef HAVE_REDISS_STORAGE_BACKEND
m_ssl_context(nullptr, redisFreeSSLContext),
#endif
m_context(nullptr, redisFree)
{
#ifdef HAVE_REDISS_STORAGE_BACKEND
ASSERT(url.scheme() == "redis" || url.scheme() == "redis+unix"
|| url.scheme() == "rediss");
#else
ASSERT(url.scheme() == "redis" || url.scheme() == "redis+unix");
#endif
if (url.scheme() == "redis+unix" && !url.host().empty()
&& url.host() != "localhost") {
throw core::Fatal(
Expand All @@ -130,11 +165,20 @@ RedisStorageBackend::RedisStorageBackend(
url.host()));
}

std::optional<std::string> cacert;
std::optional<std::string> cert;
std::optional<std::string> key;
auto connect_timeout = k_default_connect_timeout;
auto operation_timeout = k_default_operation_timeout;

for (const auto& attr : attributes) {
if (attr.key == "connect-timeout") {
if (attr.key == "cacert") {
cacert = attr.value;
} else if (attr.key == "cert") {
cert = attr.value;
} else if (attr.key == "key") {
key = attr.value;
} else if (attr.key == "connect-timeout") {
connect_timeout = parse_timeout_attribute(attr.value);
} else if (attr.key == "operation-timeout") {
operation_timeout = parse_timeout_attribute(attr.value);
Expand All @@ -143,9 +187,15 @@ RedisStorageBackend::RedisStorageBackend(
}
}

#ifdef HAVE_REDISS_STORAGE_BACKEND
init_ssl(url, cacert, cert, key);
#endif
connect(url,
static_cast<uint32_t>(connect_timeout.count()),
static_cast<uint32_t>(operation_timeout.count()));
#ifdef HAVE_REDISS_STORAGE_BACKEND
initiate_ssl(url);
#endif
authenticate(url);
select_database(url);
}
Expand Down Expand Up @@ -235,6 +285,33 @@ RedisStorageBackend::remove(const Hash::Digest& key)
}
}

#ifdef HAVE_REDISS_STORAGE_BACKEND
void
RedisStorageBackend::init_ssl(const Url& url,
std::optional<std::string> ca_cert,
std::optional<std::string> cert,
std::optional<std::string> key)
{
if (is_secure(url)) {
if (redisInitOpenSSL() != REDIS_OK) {
throw Failed("Redis SSL init OpenSSL failed");
}
redisSSLContextError ssl_error;
m_ssl_context.reset(
redisCreateSSLContext((ca_cert ? ca_cert->c_str() : NULL),
NULL,
(cert ? cert->c_str() : NULL),
(key ? key->c_str() : NULL),
NULL,
&ssl_error));
if (!m_ssl_context) {
throw Failed(FMT("Redis context construction error: {}",
redisSSLContextGetError(ssl_error)));
}
}
}
#endif

void
RedisStorageBackend::connect(const Url& url,
const uint32_t connect_timeout,
Expand Down Expand Up @@ -283,6 +360,19 @@ RedisStorageBackend::connect(const Url& url,
LOG_RAW("Redis connection OK");
}

#ifdef HAVE_REDISS_STORAGE_BACKEND
void
RedisStorageBackend::initiate_ssl(const Url& url)
{
if (is_secure(url)) {
if (redisInitiateSSLWithContext(m_context.get(), m_ssl_context.get())
!= REDIS_OK) {
throw Failed("Failed to initiate ssl");
}
}
}
#endif

void
RedisStorageBackend::select_database(const Url& url)
{
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ addtest(remote_http)
addtest(remote_only)
addtest(remote_redis)
addtest(remote_redis_unix)
addtest(remote_rediss)
addtest(remote_url)
addtest(sanitize_blacklist)
addtest(serialize_diagnostics)
Expand Down