diff --git a/.cproject b/.cproject
index 1e2acbd..6d6265b 100644
--- a/.cproject
+++ b/.cproject
@@ -22,8 +22,12 @@
-
+
@@ -35,6 +39,7 @@
+
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6153e60..576610c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,7 +40,7 @@ jobs:
path: xchange
- name: Install build dependencies
- run: sudo apt-get install libpopt-dev libreadline-dev libbsd-dev
+ run: sudo apt-get install libpopt-dev libreadline-dev libbsd-dev libssl-dev
- name: Build static library
run: make static
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b161b71..fa01c2d 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -70,6 +70,9 @@ jobs:
languages: ${{ matrix.language }}
build-mode: manual
+ - name: Install build dependencies
+ run: sudo apt-get install libpopt-dev libreadline-dev libbsd-dev libssl-dev
+
- name: Manual build
shell: bash
env:
@@ -77,6 +80,7 @@ jobs:
run: |
make -C xchange shared
make shared
+ make tools
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml
index 8d3f6e4..006c8dd 100644
--- a/.github/workflows/install.yml
+++ b/.github/workflows/install.yml
@@ -29,7 +29,7 @@ jobs:
CC: gcc
steps:
- name: install build deps
- run: sudo apt-get install doxygen libpopt-dev libreadline-dev libbsd-dev
+ run: sudo apt-get install doxygen libpopt-dev libreadline-dev libbsd-dev libssl-dev
- name: Check out RedisX
uses: actions/checkout@v4
diff --git a/Makefile b/Makefile
index f839517..e93c80a 100644
--- a/Makefile
+++ b/Makefile
@@ -96,7 +96,8 @@ distclean:
SOURCES = $(SRC)/redisx.c $(SRC)/resp.c $(SRC)/redisx-net.c $(SRC)/redisx-hooks.c \
$(SRC)/redisx-client.c $(SRC)/redisx-sentinel.c $(SRC)/redisx-cluster.c \
- $(SRC)/redisx-tab.c $(SRC)/redisx-sub.c $(SRC)/redisx-script.c
+ $(SRC)/redisx-tab.c $(SRC)/redisx-sub.c $(SRC)/redisx-script.c \
+ $(SRC)/redisx-tls.c
# Generate a list of object (obj/*.o) files from the input sources
OBJECTS := $(subst $(SRC),$(OBJ),$(SOURCES))
diff --git a/config.mk b/config.mk
index 4e85aac..1f59217 100644
--- a/config.mk
+++ b/config.mk
@@ -26,6 +26,9 @@ CC ?= gcc
# Whether to use OpenMP
WITH_OPENMP ?= 1
+# Whether to build with TLS support (via OpenSSL)
+WITH_TLS ?= 1
+
# Add include/ directory
CPPFLAGS += -I$(INC)
@@ -72,6 +75,11 @@ ifeq ($(WITH_OPENMP),1)
LDFLAGS += -fopenmp
endif
+ifeq ($(WITH_TLS),1)
+ CPPFLAGS += -DWITH_TLS=1
+ LDFLAGS += -lssl
+endif
+
# Search for libraries under LIB
ifneq ($(findstring $(LIB),$(LD_LIBRARY_PATH)),$LIB)
LDFLAGS += -L$(LIB)
diff --git a/include/redisx-priv.h b/include/redisx-priv.h
index b451757..215a3be 100644
--- a/include/redisx-priv.h
+++ b/include/redisx-priv.h
@@ -15,9 +15,12 @@
#include
#include
+#if WITH_TLS
+# include
+#endif
#define __XCHANGE_INTERNAL_API__
-#include
+#include "redisx.h"
#define IP_ADDRESS_LENGTH 40 ///< IPv6: 39 chars + termination.
@@ -49,6 +52,10 @@ typedef struct {
int available; ///< Number of bytes available in the buffer.
int next; ///< Index of next unconsumed byte in buffer.
int socket; ///< Changing the socket should require both locks!
+#if WITH_TLS
+ SSL_CTX *ctx;
+ SSL *ssl;
+#endif
int pendingRequests; ///< Number of request sent and not yet answered...
RESP *attributes; ///< Attributes from the last packet received.
} ClientPrivate;
@@ -60,6 +67,16 @@ typedef struct {
int timeoutMillis; ///< [ms] Connection timeout for sentinel nodes.
} RedisSentinel;
+#if WITH_TLS
+typedef struct {
+ boolean enabled; ///< Whether TLS is enabled.
+ char *certificate; ///< Certificate (mutual TLS only)
+ char *key; ///< Private key (mutual TLS only)
+ char *ca_certificate; ///< CA sertificate
+ char *dh_params; ///< (optional) parameter file for DH based cyphers
+} TLSConfig;
+#endif
+
/**
* A set of configuration settings for a Redis server connection.
*
@@ -74,6 +91,10 @@ typedef struct {
boolean hello; ///< whether to use HELLO (introduced in Redis 6.0.0 only)
RedisSocketConfigurator socketConf; ///< Additional user configuration of client sockets
+#if WITH_TLS
+ TLSConfig tls; ///< TLS configuration settings
+#endif
+
Hook *firstCleanupCall; ///< Linked list of cleanup calls
Hook *firstConnectCall; ///< Linked list of connection calls
diff --git a/include/redisx.h b/include/redisx.h
index 81d032a..b7e3fa0 100644
--- a/include/redisx.h
+++ b/include/redisx.h
@@ -404,6 +404,9 @@ Redis *redisxInitSentinel(const char *serviceName, const RedisServer *serverList
int redisxValidateSentinel(const char *serviceName, const RedisServer *serverList, int nServers);
int redisxCheckValid(const Redis *redis);
void redisxDestroy(Redis *redis);
+int redisxSetTLS(Redis *redis, const char *ca_file);
+int redisxSetMutualTLS(Redis *redis, const char *cert_file, const char *key_file);
+int redisxSetDHParams(Redis *redis, const char *dh_params_file);
int redisxConnect(Redis *redis, boolean usePipeline);
void redisxDisconnect(Redis *redis);
int redisxReconnect(Redis *redis, boolean usePipeline);
diff --git a/requirements.apt b/requirements.apt
index 627f918..18f7194 100644
--- a/requirements.apt
+++ b/requirements.apt
@@ -1,4 +1,5 @@
# sudo apt install $(grep -v ^# requirements.apt)
+libssl-dev
libpopt-dev
libreadline-dev
libbsd-dev
diff --git a/requirements.dnf b/requirements.dnf
index cec0897..73109b0 100644
--- a/requirements.dnf
+++ b/requirements.dnf
@@ -1,4 +1,5 @@
# sudo dnf install $(grep -v ^# requirements.dnf)
+openssl-devel
popt-devel
readline-devel
libbsd-devel
diff --git a/src/redisx-client.c b/src/redisx-client.c
index f08c0d0..9718a54 100644
--- a/src/redisx-client.c
+++ b/src/redisx-client.c
@@ -18,6 +18,9 @@
#else
# include
#endif
+#if WITH_TLS
+# include
+#endif
#include "redisx-priv.h"
@@ -104,6 +107,10 @@ static int rReadChunkAsync(ClientPrivate *cp) {
errno = 0;
cp->next = 0;
+#if WITH_TLS
+ if(cp->ssl) cp->available = SSL_read(cp->ssl, cp->in, REDISX_RCVBUF_SIZE);
+ else
+#endif
cp->available = recv(sock, cp->in, REDISX_RCVBUF_SIZE, 0);
trprintf(" ... read %d bytes from client %d socket.\n", cp->available, cp->idx);
if(cp->available <= 0) {
@@ -272,6 +279,10 @@ static int rSendBytesAsync(ClientPrivate *cp, const char *buf, int length, boole
while(length > 0) {
int n;
+#if WITH_TLS
+ if(cp->ssl) n = SSL_write(cp->ssl, from, length);
+ else
+#endif
#if __linux__
// Linux supports flagging outgoing messages to inform it whether or not more
// imminent data is on its way
@@ -283,7 +294,7 @@ static int rSendBytesAsync(ClientPrivate *cp, const char *buf, int length, boole
n = send(sock, from, length, 0);
#endif
- if(n < 0) {
+ if(n <= 0) {
int status = rTransmitErrorAsync(cp, "send");
if(cp->isEnabled) x_trace(fn, NULL, status);
cp->isEnabled = FALSE;
diff --git a/src/redisx-net.c b/src/redisx-net.c
index fa1d999..eaa5f38 100644
--- a/src/redisx-net.c
+++ b/src/redisx-net.c
@@ -340,6 +340,10 @@ static void rDisconnectClientAsync(RedisClient *cl) {
*
*/
static void rResetClientAsync(RedisClient *cl) {
+#if(WITH_TLS)
+ extern void rDestroyClientTLS(ClientPrivate *cp);
+#endif
+
ClientPrivate *cp = (ClientPrivate *) cl->priv;
pthread_mutex_lock(&cp->pendingLock);
@@ -349,6 +353,12 @@ static void rResetClientAsync(RedisClient *cl) {
cp->isEnabled = FALSE;
cp->available = 0;
cp->next = 0;
+
+#if(WITH_TLS)
+ rDestroyClientTLS(cp);
+#endif
+
+
cp->socket = -1; // Reset the channel's socket descriptor to 'unassigned'
}
@@ -611,6 +621,10 @@ static int rHelloAsync(RedisClient *cl, char *clientID) {
int rConnectClient(Redis *redis, enum redisx_channel channel) {
static const char *fn = "rConnectClient";
+#if WITH_TLS
+ extern int rConnectTLSClient(ClientPrivate *cp, const TLSConfig *tls);
+#endif
+
struct sockaddr_in serverAddress;
struct utsname u;
RedisPrivate *p;
@@ -646,6 +660,13 @@ int rConnectClient(Redis *redis, enum redisx_channel channel) {
prop_error(fn, config->socketConf(sock, channel));
}
+#if WITH_TLS
+ if(config->tls.enabled && rConnectTLSClient(cp, &config->tls) != X_SUCCESS) {
+ close(sock);
+ return x_error(X_NO_INIT, errno, fn, "failed to connect (with TLS) to %s:%hu: %s", redis->id, port, strerror(errno));
+ }
+ else
+#endif
if(connect(sock, (struct sockaddr *) &serverAddress, sizeof(serverAddress)) < 0) {
close(sock);
return x_error(X_NO_INIT, errno, fn, "failed to connect to %s:%hu: %s", redis->id, port, strerror(errno));
diff --git a/src/redisx-tls.c b/src/redisx-tls.c
new file mode 100644
index 0000000..110b501
--- /dev/null
+++ b/src/redisx-tls.c
@@ -0,0 +1,262 @@
+/**
+ * @file
+ *
+ * @date Created on Jan 6, 2025
+ * @author Attila Kovacs
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+#if WITH_TLS
+# include
+# include
+#endif
+
+#include "redisx-priv.h"
+
+#if WITH_TLS
+/// \cond PRIVATE
+
+static int initialized = FALSE;
+
+/**
+ * Shuts down SSL and frees up the SSL-related resources on a client.
+ *
+ * @param cp Private client data
+ *
+ * @sa rConnectTLSClient()
+ */
+void rDestroyClientTLS(ClientPrivate *cp) {
+ if(cp->ssl) {
+ SSL_shutdown(cp->ssl);
+ cp->ssl = NULL;
+ }
+
+ if(cp->ctx) {
+ SSL_CTX_free(cp->ctx);
+ cp->ctx = NULL;
+ }
+}
+
+/**
+ * Connects a client using the specified TLS configuration.
+ *
+ * @param cp Private client data
+ * @param tls TLS configuration
+ * @return X_SUCCESS (0) if successful, or else an error code <0.
+ */
+int rConnectTLSClient(ClientPrivate *cp, const TLSConfig *tls) {
+ static const char *fn = "rConnectClientTLS";
+ static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+
+ const SSL_METHOD *method;
+ X509 *server_cert;
+
+ if(!tls->certificate) return x_error(X_NULL, EINVAL, fn, "certificate is NULL");
+
+ // Initialize SSL lib only once...
+ pthread_mutex_lock(&mutex);
+ if(!initialized) {
+ SSL_library_init();
+ SSL_load_error_strings();
+ SSLeay_add_ssl_algorithms();
+ initialized = TRUE;
+ }
+ pthread_mutex_unlock(&mutex);
+
+ method = TLS_client_method();
+ cp->ctx = SSL_CTX_new(method);
+ if (!cp->ctx) {
+ perror("Unable to create SSL context");
+ ERR_print_errors_fp(stderr);
+ goto abort; // @suppress("Goto statement used")
+ }
+
+ if(tls->certificate && tls->key) {
+ /* Set the key and cert */
+ if (SSL_CTX_use_certificate_file(cp->ctx, tls->certificate, SSL_FILETYPE_PEM) <= 0) {
+ ERR_print_errors_fp(stderr);
+ goto abort; // @suppress("Goto statement used")
+ }
+
+ if (SSL_CTX_use_PrivateKey_file(cp->ctx, tls->key, SSL_FILETYPE_PEM) <= 0 ) {
+ ERR_print_errors_fp(stderr);
+ goto abort; // @suppress("Goto statement used")
+ }
+ }
+
+ if(tls->dh_params) if(!SSL_CTX_set_tmp_dh(cp->ctx, tls->dh_params))
+ goto abort; // @suppress("Goto statement used")
+
+ SSL_set_fd(cp->ssl, cp->socket);
+ if(!SSL_connect(cp->ssl)) goto abort; // @suppress("Goto statement used")
+
+ server_cert = SSL_get_peer_certificate(cp->ssl);
+ if(!server_cert) goto abort; // @suppress("Goto statement used")
+
+ if(redisxIsVerbose()) {
+ printf("Server certificate: \n");
+ char *str = X509_NAME_oneline(X509_get_subject_name(server_cert), 0, 0);
+ if(str) {
+ printf("\tsubject: %s\n", str);
+ OPENSSL_free(str);
+ }
+ else printf("\n");
+
+ str = X509_NAME_oneline(X509_get_issuer_name(server_cert), 0, 0);
+ if(str) {
+ printf("\tissuer: %s\n", str);
+ OPENSSL_free(str);
+ }
+ else printf("\n");
+ }
+
+ X509_free(server_cert);
+
+ return X_SUCCESS;
+
+ // -------------------------------------------------------------------------
+
+ abort:
+
+ rDestroyClientTLS(cp);
+ return x_error(X_FAILURE, errno, fn, "TLS connection failed.");
+}
+
+#endif
+
+/// \endcond
+
+/**
+ * Configures a TLS-enrypted connection to Redis with the specified CA certificate file. Normally you
+ * will want to set up mutual TLS with redisxSetMutualTLS() also, unless the server is not requiring
+ * mutual authentication. Additionally, you might also want to set parameters for DH-based cyphers if
+ * needed using redisxSetDHParams().
+ *
+ * @param redis A Redis instance
+ * @param ca_file Path to the CA certificate file
+ * @return X_SUCCESS (0) if successful, or else an error code <0.
+ *
+ * @sa redisxSetMutualTLS()
+ * @sa redisxSetDHParams()
+ */
+int redisxSetTLS(Redis *redis, const char *ca_file) {
+ static const char *fn = "redisxSetCA";
+
+#if WITH_TLS
+ RedisPrivate *p;
+ TLSConfig *tls;
+
+ if(!ca_file) return x_error(X_NULL, EINVAL, fn, "CA file is NULL");
+
+ if(access(ca_file, R_OK) != 0) return x_error(X_FAILURE, errno, fn, "CA file not readable: %s", ca_file);
+
+ prop_error(fn, rConfigLock(redis));
+
+ p = (RedisPrivate *) redis->priv;
+ tls = &p->config.tls;
+
+ tls->enabled = TRUE;
+ tls->ca_certificate = (char *) ca_file;
+
+ rConfigUnlock(redis);
+
+ return X_SUCCESS;
+#else
+ (void) redis;
+ (void) ca_file;
+
+ return x_error(X_FAILURE, ENOSYS, fn, "RedisX was built without TLS support");
+#endif
+}
+
+/**
+ * Set a TLS certificate and private key for mutual TLS. You will still need to call redisxSetTLS() also to create a
+ * complete TLS configuration. Redis normally uses mutual TLS, which requires both the client and the server to
+ * authenticate themselves. For this you need the server's TLS certificate and private key also. It is possible to
+ * configure Redis servers to verify one way only with a CA certificate, in which case you don't need to call this to
+ * configure the client.
+ *
+ * @param redis A Redis instance
+ * @param cert_file Path to the server's certificate file
+ * @param key_file Path to the server'sprivate key file
+ * @return X_SUCCESS (0) if successful, or else an error code <0.
+ *
+ * @sa redisxSetTLS()
+ */
+int redisxSetMutualTLS(Redis *redis, const char *cert_file, const char *key_file) {
+ static const char *fn = "redisxSetMutualTLS";
+
+#if WITH_TLS
+ RedisPrivate *p;
+ TLSConfig *tls;
+
+ if(!cert_file || !key_file) return x_error(X_NULL, EINVAL, fn, "Null parameter(s): cert_file=%p, key_file=%p", cert_file, key_file);
+
+ if(access(cert_file, R_OK) != 0) return x_error(X_FAILURE, errno, fn, "Certificate file not readable: %s", cert_file);
+ if(access(key_file, R_OK) != 0) return x_error(X_FAILURE, errno, fn, "Private key file not readable: %s", key_file);
+
+ prop_error(fn, rConfigLock(redis));
+
+ p = (RedisPrivate *) redis->priv;
+ tls = &p->config.tls;
+
+ tls->certificate = (char *) cert_file;
+ tls->key = (char *) key_file;
+
+ rConfigUnlock(redis);
+
+ return X_SUCCESS;
+#else
+ (void) redis;
+ (void) cert_file;
+ (void) key_file;
+
+ return x_error(X_FAILURE, ENOSYS, fn, "RedisX was built without TLS support");
+#endif
+}
+
+
+/**
+ * Sets parameters for DH-based cyphers when using a TLS encrypted connection to Redis.
+ *
+ * @param redis A Redis instance
+ * @param dh_params_file Path to the DH-based cypher parameters file
+ * @return X_SUCCESS (0) if successful, or else an error code <0.
+ *
+ * @sa redisxSetTLS()
+ */
+int redisxSetDHParams(Redis *redis, const char *dh_params_file) {
+ static const char *fn = "redisxSetDHParams";
+
+#if WITH_TLS
+ RedisPrivate *p;
+ TLSConfig *tls;
+
+ if(!dh_params_file) return x_error(X_NULL, EINVAL, fn, "DH parameters file is NULL");
+
+ if(access(dh_params_file, R_OK) != 0) return x_error(X_FAILURE, errno, fn, "CA file not readable: %s", dh_params_file);
+
+ prop_error(fn, rConfigLock(redis));
+
+ p = (RedisPrivate *) redis->priv;
+ tls = &p->config.tls;
+
+ tls->dh_params = (char *) dh_params_file;
+
+ rConfigUnlock(redis);
+
+ return X_SUCCESS;
+#else
+ (void) redis;
+ (void) dh_params_file;
+
+ return x_error(X_FAILURE, ENOSYS, fn, "RedisX was built without TLS support");
+#endif
+}
+
+