Unverified Commit 3f073b1d authored by Yossi Gottlieb's avatar Yossi Gottlieb Committed by GitHub
Browse files

Merge pull request #7609 from michael-grunder/redis-cli-resp3-push

Add redis-cli RESP3 Push support
parents 3f494cc4 81879bc1
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
* the include of your alternate allocator if needed (not needed in order * the include of your alternate allocator if needed (not needed in order
* to use the default libc allocator). */ * to use the default libc allocator). */
#define s_malloc malloc #include "alloc.h"
#define s_realloc realloc
#define s_free free #define s_malloc hi_malloc
#define s_realloc hi_realloc
#define s_free hi_free
...@@ -212,7 +212,7 @@ int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, sockle ...@@ -212,7 +212,7 @@ int win32_getsockopt(SOCKET sockfd, int level, int optname, void *optval, sockle
int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen) { int win32_setsockopt(SOCKET sockfd, int level, int optname, const void *optval, socklen_t optlen) {
int ret = 0; int ret = 0;
if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) { if ((level == SOL_SOCKET) && ((optname == SO_RCVTIMEO) || (optname == SO_SNDTIMEO))) {
struct timeval *tv = optval; const struct timeval *tv = optval;
DWORD timeout = tv->tv_sec * 1000 + tv->tv_usec / 1000; DWORD timeout = tv->tv_sec * 1000 + tv->tv_usec / 1000;
ret = setsockopt(sockfd, level, optname, (const char*)&timeout, sizeof(DWORD)); ret = setsockopt(sockfd, level, optname, (const char*)&timeout, sizeof(DWORD));
} else { } else {
......
...@@ -49,9 +49,10 @@ ...@@ -49,9 +49,10 @@
#include <winsock2.h> #include <winsock2.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <stddef.h> #include <stddef.h>
#include <errno.h>
#ifdef _MSC_VER #ifdef _MSC_VER
typedef signed long ssize_t; typedef long long ssize_t;
#endif #endif
/* Emulate the parts of the BSD socket API that we need (override the winsock signatures). */ /* Emulate the parts of the BSD socket API that we need (override the winsock signatures). */
......
...@@ -34,25 +34,33 @@ ...@@ -34,25 +34,33 @@
#include "async.h" #include "async.h"
#include <assert.h> #include <assert.h>
#include <pthread.h>
#include <errno.h> #include <errno.h>
#include <string.h> #include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <openssl/err.h> #include <openssl/err.h>
#include "win32.h"
#include "async_private.h" #include "async_private.h"
#include "hiredis_ssl.h"
void __redisSetError(redisContext *c, int type, const char *str); void __redisSetError(redisContext *c, int type, const char *str);
/* The SSL context is attached to SSL/TLS connections as a privdata. */ struct redisSSLContext {
typedef struct redisSSLContext { /* Associated OpenSSL SSL_CTX as created by redisCreateSSLContext() */
/**
* OpenSSL SSL_CTX; It is optional and will not be set when using
* user-supplied SSL.
*/
SSL_CTX *ssl_ctx; SSL_CTX *ssl_ctx;
/* Requested SNI, or NULL */
char *server_name;
};
/* The SSL connection context is attached to SSL/TLS connections as a privdata. */
typedef struct redisSSL {
/** /**
* OpenSSL SSL object. * OpenSSL SSL object.
*/ */
...@@ -72,43 +80,11 @@ typedef struct redisSSLContext { ...@@ -72,43 +80,11 @@ typedef struct redisSSLContext {
* should resume whenever a read takes place, if possible * should resume whenever a read takes place, if possible
*/ */
int pendingWrite; int pendingWrite;
} redisSSLContext; } redisSSL;
/* Forward declaration */ /* Forward declaration */
redisContextFuncs redisContextSSLFuncs; redisContextFuncs redisContextSSLFuncs;
#ifdef HIREDIS_SSL_TRACE
/**
* Callback used for debugging
*/
static void sslLogCallback(const SSL *ssl, int where, int ret) {
const char *retstr = "";
int should_log = 1;
/* Ignore low-level SSL stuff */
if (where & SSL_CB_ALERT) {
should_log = 1;
}
if (where == SSL_CB_HANDSHAKE_START || where == SSL_CB_HANDSHAKE_DONE) {
should_log = 1;
}
if ((where & SSL_CB_EXIT) && ret == 0) {
should_log = 1;
}
if (!should_log) {
return;
}
retstr = SSL_alert_type_string(ret);
printf("ST(0x%x). %s. R(0x%x)%s\n", where, SSL_state_string_long(ssl), ret, retstr);
if (where == SSL_CB_HANDSHAKE_DONE) {
printf("Using SSL version %s. Cipher=%s\n", SSL_get_version(ssl), SSL_get_cipher_name(ssl));
}
}
#endif
/** /**
* OpenSSL global initialization and locking handling callbacks. * OpenSSL global initialization and locking handling callbacks.
* Note that this is only required for OpenSSL < 1.1.0. * Note that this is only required for OpenSSL < 1.1.0.
...@@ -119,6 +95,18 @@ static void sslLogCallback(const SSL *ssl, int where, int ret) { ...@@ -119,6 +95,18 @@ static void sslLogCallback(const SSL *ssl, int where, int ret) {
#endif #endif
#ifdef HIREDIS_USE_CRYPTO_LOCKS #ifdef HIREDIS_USE_CRYPTO_LOCKS
#ifdef _WIN32
typedef CRITICAL_SECTION sslLockType;
static void sslLockInit(sslLockType* l) {
InitializeCriticalSection(l);
}
static void sslLockAcquire(sslLockType* l) {
EnterCriticalSection(l);
}
static void sslLockRelease(sslLockType* l) {
LeaveCriticalSection(l);
}
#else
typedef pthread_mutex_t sslLockType; typedef pthread_mutex_t sslLockType;
static void sslLockInit(sslLockType *l) { static void sslLockInit(sslLockType *l) {
pthread_mutex_init(l, NULL); pthread_mutex_init(l, NULL);
...@@ -129,7 +117,9 @@ static void sslLockAcquire(sslLockType *l) { ...@@ -129,7 +117,9 @@ static void sslLockAcquire(sslLockType *l) {
static void sslLockRelease(sslLockType *l) { static void sslLockRelease(sslLockType *l) {
pthread_mutex_unlock(l); pthread_mutex_unlock(l);
} }
static pthread_mutex_t *ossl_locks; #endif
static sslLockType* ossl_locks;
static void opensslDoLock(int mode, int lkid, const char *f, int line) { static void opensslDoLock(int mode, int lkid, const char *f, int line) {
sslLockType *l = ossl_locks + lkid; sslLockType *l = ossl_locks + lkid;
...@@ -144,36 +134,151 @@ static void opensslDoLock(int mode, int lkid, const char *f, int line) { ...@@ -144,36 +134,151 @@ static void opensslDoLock(int mode, int lkid, const char *f, int line) {
(void)line; (void)line;
} }
static void initOpensslLocks(void) { static int initOpensslLocks(void) {
unsigned ii, nlocks; unsigned ii, nlocks;
if (CRYPTO_get_locking_callback() != NULL) { if (CRYPTO_get_locking_callback() != NULL) {
/* Someone already set the callback before us. Don't destroy it! */ /* Someone already set the callback before us. Don't destroy it! */
return; return REDIS_OK;
} }
nlocks = CRYPTO_num_locks(); nlocks = CRYPTO_num_locks();
ossl_locks = malloc(sizeof(*ossl_locks) * nlocks); ossl_locks = hi_malloc(sizeof(*ossl_locks) * nlocks);
if (ossl_locks == NULL)
return REDIS_ERR;
for (ii = 0; ii < nlocks; ii++) { for (ii = 0; ii < nlocks; ii++) {
sslLockInit(ossl_locks + ii); sslLockInit(ossl_locks + ii);
} }
CRYPTO_set_locking_callback(opensslDoLock); CRYPTO_set_locking_callback(opensslDoLock);
return REDIS_OK;
} }
#endif /* HIREDIS_USE_CRYPTO_LOCKS */ #endif /* HIREDIS_USE_CRYPTO_LOCKS */
int redisInitOpenSSL(void)
{
SSL_library_init();
#ifdef HIREDIS_USE_CRYPTO_LOCKS
initOpensslLocks();
#endif
return REDIS_OK;
}
/**
* redisSSLContext helper context destruction.
*/
const char *redisSSLContextGetError(redisSSLContextError error)
{
switch (error) {
case REDIS_SSL_CTX_NONE:
return "No Error";
case REDIS_SSL_CTX_CREATE_FAILED:
return "Failed to create OpenSSL SSL_CTX";
case REDIS_SSL_CTX_CERT_KEY_REQUIRED:
return "Client cert and key must both be specified or skipped";
case REDIS_SSL_CTX_CA_CERT_LOAD_FAILED:
return "Failed to load CA Certificate or CA Path";
case REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED:
return "Failed to load client certificate";
case REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED:
return "Failed to load private key";
default:
return "Unknown error code";
}
}
void redisFreeSSLContext(redisSSLContext *ctx)
{
if (!ctx)
return;
if (ctx->server_name) {
hi_free(ctx->server_name);
ctx->server_name = NULL;
}
if (ctx->ssl_ctx) {
SSL_CTX_free(ctx->ssl_ctx);
ctx->ssl_ctx = NULL;
}
hi_free(ctx);
}
/**
* redisSSLContext helper context initialization.
*/
redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *capath,
const char *cert_filename, const char *private_key_filename,
const char *server_name, redisSSLContextError *error)
{
redisSSLContext *ctx = hi_calloc(1, sizeof(redisSSLContext));
if (ctx == NULL)
goto error;
ctx->ssl_ctx = SSL_CTX_new(SSLv23_client_method());
if (!ctx->ssl_ctx) {
if (error) *error = REDIS_SSL_CTX_CREATE_FAILED;
goto error;
}
SSL_CTX_set_options(ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
SSL_CTX_set_verify(ctx->ssl_ctx, SSL_VERIFY_PEER, NULL);
if ((cert_filename != NULL && private_key_filename == NULL) ||
(private_key_filename != NULL && cert_filename == NULL)) {
if (error) *error = REDIS_SSL_CTX_CERT_KEY_REQUIRED;
goto error;
}
if (capath || cacert_filename) {
if (!SSL_CTX_load_verify_locations(ctx->ssl_ctx, cacert_filename, capath)) {
if (error) *error = REDIS_SSL_CTX_CA_CERT_LOAD_FAILED;
goto error;
}
}
if (cert_filename) {
if (!SSL_CTX_use_certificate_chain_file(ctx->ssl_ctx, cert_filename)) {
if (error) *error = REDIS_SSL_CTX_CLIENT_CERT_LOAD_FAILED;
goto error;
}
if (!SSL_CTX_use_PrivateKey_file(ctx->ssl_ctx, private_key_filename, SSL_FILETYPE_PEM)) {
if (error) *error = REDIS_SSL_CTX_PRIVATE_KEY_LOAD_FAILED;
goto error;
}
}
if (server_name)
ctx->server_name = hi_strdup(server_name);
return ctx;
error:
redisFreeSSLContext(ctx);
return NULL;
}
/** /**
* SSL Connection initialization. * SSL Connection initialization.
*/ */
static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
if (c->privdata) { static int redisSSLConnect(redisContext *c, SSL *ssl) {
if (c->privctx) {
__redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated"); __redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated");
return REDIS_ERR; return REDIS_ERR;
} }
c->privdata = calloc(1, sizeof(redisSSLContext));
c->funcs = &redisContextSSLFuncs; redisSSL *rssl = hi_calloc(1, sizeof(redisSSL));
redisSSLContext *rssl = c->privdata; if (rssl == NULL) {
__redisSetError(c, REDIS_ERR_OOM, "Out of memory");
return REDIS_ERR;
}
rssl->ssl_ctx = ssl_ctx; c->funcs = &redisContextSSLFuncs;
rssl->ssl = ssl; rssl->ssl = ssl;
SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
...@@ -183,12 +288,14 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { ...@@ -183,12 +288,14 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
ERR_clear_error(); ERR_clear_error();
int rv = SSL_connect(rssl->ssl); int rv = SSL_connect(rssl->ssl);
if (rv == 1) { if (rv == 1) {
c->privctx = rssl;
return REDIS_OK; return REDIS_OK;
} }
rv = SSL_get_error(rssl->ssl, rv); rv = SSL_get_error(rssl->ssl, rv);
if (((c->flags & REDIS_BLOCK) == 0) && if (((c->flags & REDIS_BLOCK) == 0) &&
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) { (rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
c->privctx = rssl;
return REDIS_OK; return REDIS_OK;
} }
...@@ -203,83 +310,58 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { ...@@ -203,83 +310,58 @@ static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) {
} }
__redisSetError(c, REDIS_ERR_IO, err); __redisSetError(c, REDIS_ERR_IO, err);
} }
hi_free(rssl);
return REDIS_ERR; return REDIS_ERR;
} }
/**
* A wrapper around redisSSLConnect() for users who manage their own context and
* create their own SSL object.
*/
int redisInitiateSSL(redisContext *c, SSL *ssl) { int redisInitiateSSL(redisContext *c, SSL *ssl) {
return redisSSLConnect(c, NULL, ssl); return redisSSLConnect(c, ssl);
} }
int redisSecureConnection(redisContext *c, const char *capath, /**
const char *certpath, const char *keypath, const char *servername) { * A wrapper around redisSSLConnect() for users who use redisSSLContext and don't
* manage their own SSL objects.
SSL_CTX *ssl_ctx = NULL; */
SSL *ssl = NULL;
/* Initialize global OpenSSL stuff */
static int isInit = 0;
if (!isInit) {
isInit = 1;
SSL_library_init();
#ifdef HIREDIS_USE_CRYPTO_LOCKS
initOpensslLocks();
#endif
}
ssl_ctx = SSL_CTX_new(SSLv23_client_method());
if (!ssl_ctx) {
__redisSetError(c, REDIS_ERR_OTHER, "Failed to create SSL_CTX");
goto error;
}
#ifdef HIREDIS_SSL_TRACE int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx)
SSL_CTX_set_info_callback(ssl_ctx, sslLogCallback); {
#endif if (!c || !redis_ssl_ctx)
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); return REDIS_ERR;
SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);
if ((certpath != NULL && keypath == NULL) || (keypath != NULL && certpath == NULL)) {
__redisSetError(c, REDIS_ERR_OTHER, "certpath and keypath must be specified together");
goto error;
}
if (capath) { /* We want to verify that redisSSLConnect() won't fail on this, as it will
if (!SSL_CTX_load_verify_locations(ssl_ctx, capath, NULL)) { * not own the SSL object in that case and we'll end up leaking.
__redisSetError(c, REDIS_ERR_OTHER, "Invalid CA certificate"); */
goto error; if (c->privctx)
} return REDIS_ERR;
}
if (certpath) {
if (!SSL_CTX_use_certificate_chain_file(ssl_ctx, certpath)) {
__redisSetError(c, REDIS_ERR_OTHER, "Invalid client certificate");
goto error;
}
if (!SSL_CTX_use_PrivateKey_file(ssl_ctx, keypath, SSL_FILETYPE_PEM)) {
__redisSetError(c, REDIS_ERR_OTHER, "Invalid client key");
goto error;
}
}
ssl = SSL_new(ssl_ctx); SSL *ssl = SSL_new(redis_ssl_ctx->ssl_ctx);
if (!ssl) { if (!ssl) {
__redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance"); __redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance");
goto error; goto error;
} }
if (servername) {
if (!SSL_set_tlsext_host_name(ssl, servername)) { if (redis_ssl_ctx->server_name) {
__redisSetError(c, REDIS_ERR_OTHER, "Couldn't set server name indication"); if (!SSL_set_tlsext_host_name(ssl, redis_ssl_ctx->server_name)) {
__redisSetError(c, REDIS_ERR_OTHER, "Failed to set server_name/SNI");
goto error; goto error;
} }
} }
return redisSSLConnect(c, ssl_ctx, ssl); return redisSSLConnect(c, ssl);
error: error:
if (ssl) SSL_free(ssl); if (ssl)
if (ssl_ctx) SSL_CTX_free(ssl_ctx); SSL_free(ssl);
return REDIS_ERR; return REDIS_ERR;
} }
static int maybeCheckWant(redisSSLContext *rssl, int rv) { static int maybeCheckWant(redisSSL *rssl, int rv) {
/** /**
* If the error is WANT_READ or WANT_WRITE, the appropriate flags are set * If the error is WANT_READ or WANT_WRITE, the appropriate flags are set
* and true is returned. False is returned otherwise * and true is returned. False is returned otherwise
...@@ -299,23 +381,19 @@ static int maybeCheckWant(redisSSLContext *rssl, int rv) { ...@@ -299,23 +381,19 @@ static int maybeCheckWant(redisSSLContext *rssl, int rv) {
* Implementation of redisContextFuncs for SSL connections. * Implementation of redisContextFuncs for SSL connections.
*/ */
static void redisSSLFreeContext(void *privdata){ static void redisSSLFree(void *privctx){
redisSSLContext *rsc = privdata; redisSSL *rsc = privctx;
if (!rsc) return; if (!rsc) return;
if (rsc->ssl) { if (rsc->ssl) {
SSL_free(rsc->ssl); SSL_free(rsc->ssl);
rsc->ssl = NULL; rsc->ssl = NULL;
} }
if (rsc->ssl_ctx) { hi_free(rsc);
SSL_CTX_free(rsc->ssl_ctx);
rsc->ssl_ctx = NULL;
}
free(rsc);
} }
static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { static ssize_t redisSSLRead(redisContext *c, char *buf, size_t bufcap) {
redisSSLContext *rssl = c->privdata; redisSSL *rssl = c->privctx;
int nread = SSL_read(rssl->ssl, buf, bufcap); int nread = SSL_read(rssl->ssl, buf, bufcap);
if (nread > 0) { if (nread > 0) {
...@@ -356,8 +434,8 @@ static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { ...@@ -356,8 +434,8 @@ static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) {
} }
} }
static int redisSSLWrite(redisContext *c) { static ssize_t redisSSLWrite(redisContext *c) {
redisSSLContext *rssl = c->privdata; redisSSL *rssl = c->privctx;
size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf); size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf);
int rv = SSL_write(rssl->ssl, c->obuf, len); int rv = SSL_write(rssl->ssl, c->obuf, len);
...@@ -380,7 +458,7 @@ static int redisSSLWrite(redisContext *c) { ...@@ -380,7 +458,7 @@ static int redisSSLWrite(redisContext *c) {
static void redisSSLAsyncRead(redisAsyncContext *ac) { static void redisSSLAsyncRead(redisAsyncContext *ac) {
int rv; int rv;
redisSSLContext *rssl = ac->c.privdata; redisSSL *rssl = ac->c.privctx;
redisContext *c = &ac->c; redisContext *c = &ac->c;
rssl->wantRead = 0; rssl->wantRead = 0;
...@@ -410,7 +488,7 @@ static void redisSSLAsyncRead(redisAsyncContext *ac) { ...@@ -410,7 +488,7 @@ static void redisSSLAsyncRead(redisAsyncContext *ac) {
static void redisSSLAsyncWrite(redisAsyncContext *ac) { static void redisSSLAsyncWrite(redisAsyncContext *ac) {
int rv, done = 0; int rv, done = 0;
redisSSLContext *rssl = ac->c.privdata; redisSSL *rssl = ac->c.privctx;
redisContext *c = &ac->c; redisContext *c = &ac->c;
rssl->pendingWrite = 0; rssl->pendingWrite = 0;
...@@ -439,7 +517,7 @@ static void redisSSLAsyncWrite(redisAsyncContext *ac) { ...@@ -439,7 +517,7 @@ static void redisSSLAsyncWrite(redisAsyncContext *ac) {
} }
redisContextFuncs redisContextSSLFuncs = { redisContextFuncs redisContextSSLFuncs = {
.free_privdata = redisSSLFreeContext, .free_privctx = redisSSLFree,
.async_read = redisSSLAsyncRead, .async_read = redisSSLAsyncRead,
.async_write = redisSSLAsyncWrite, .async_write = redisSSLAsyncWrite,
.read = redisSSLRead, .read = redisSSLRead,
......
#include "fmacros.h" #include "fmacros.h"
#include "sockcompat.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#ifndef _WIN32
#include <strings.h> #include <strings.h>
#include <sys/socket.h>
#include <sys/time.h> #include <sys/time.h>
#include <netdb.h> #endif
#include <assert.h> #include <assert.h>
#include <unistd.h>
#include <signal.h> #include <signal.h>
#include <errno.h> #include <errno.h>
#include <limits.h> #include <limits.h>
#include "hiredis.h" #include "hiredis.h"
#include "async.h"
#ifdef HIREDIS_TEST_SSL #ifdef HIREDIS_TEST_SSL
#include "hiredis_ssl.h" #include "hiredis_ssl.h"
#endif #endif
#include "net.h" #include "net.h"
#include "win32.h"
enum connection_type { enum connection_type {
CONN_TCP, CONN_TCP,
...@@ -47,15 +49,30 @@ struct config { ...@@ -47,15 +49,30 @@ struct config {
} ssl; } ssl;
}; };
struct privdata {
int dtor_counter;
};
#ifdef HIREDIS_TEST_SSL
redisSSLContext *_ssl_ctx = NULL;
#endif
/* The following lines make up our testing "framework" :) */ /* The following lines make up our testing "framework" :) */
static int tests = 0, fails = 0; static int tests = 0, fails = 0, skips = 0;
#define test(_s) { printf("#%02d ", ++tests); printf(_s); } #define test(_s) { printf("#%02d ", ++tests); printf(_s); }
#define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;} #define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;}
#define test_skipped() { printf("\033[01;33mSKIPPED\033[0;0m\n"); skips++; }
static long long usec(void) { static long long usec(void) {
#ifndef _MSC_VER
struct timeval tv; struct timeval tv;
gettimeofday(&tv,NULL); gettimeofday(&tv,NULL);
return (((long long)tv.tv_sec)*1000000)+tv.tv_usec; return (((long long)tv.tv_sec)*1000000)+tv.tv_usec;
#else
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
return (((long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime) / 10;
#endif
} }
/* The assert() calls below have side effects, so we need assert() /* The assert() calls below have side effects, so we need assert()
...@@ -65,6 +82,43 @@ static long long usec(void) { ...@@ -65,6 +82,43 @@ static long long usec(void) {
#define assert(e) (void)(e) #define assert(e) (void)(e)
#endif #endif
/* Helper to extract Redis version information. Aborts on any failure. */
#define REDIS_VERSION_FIELD "redis_version:"
void get_redis_version(redisContext *c, int *majorptr, int *minorptr) {
redisReply *reply;
char *eptr, *s, *e;
int major, minor;
reply = redisCommand(c, "INFO");
if (reply == NULL || c->err || reply->type != REDIS_REPLY_STRING)
goto abort;
if ((s = strstr(reply->str, REDIS_VERSION_FIELD)) == NULL)
goto abort;
s += strlen(REDIS_VERSION_FIELD);
/* We need a field terminator and at least 'x.y.z' (5) bytes of data */
if ((e = strstr(s, "\r\n")) == NULL || (e - s) < 5)
goto abort;
/* Extract version info */
major = strtol(s, &eptr, 10);
if (*eptr != '.') goto abort;
minor = strtol(eptr+1, NULL, 10);
/* Push info the caller wants */
if (majorptr) *majorptr = major;
if (minorptr) *minorptr = minor;
freeReplyObject(reply);
return;
abort:
freeReplyObject(reply);
fprintf(stderr, "Error: Cannot determine Redis version, aborting\n");
exit(1);
}
static redisContext *select_database(redisContext *c) { static redisContext *select_database(redisContext *c) {
redisReply *reply; redisReply *reply;
...@@ -87,6 +141,26 @@ static redisContext *select_database(redisContext *c) { ...@@ -87,6 +141,26 @@ static redisContext *select_database(redisContext *c) {
return c; return c;
} }
/* Switch protocol */
static void send_hello(redisContext *c, int version) {
redisReply *reply;
int expected;
reply = redisCommand(c, "HELLO %d", version);
expected = version == 3 ? REDIS_REPLY_MAP : REDIS_REPLY_ARRAY;
assert(reply != NULL && reply->type == expected);
freeReplyObject(reply);
}
/* Togggle client tracking */
static void send_client_tracking(redisContext *c, const char *str) {
redisReply *reply;
reply = redisCommand(c, "CLIENT TRACKING %s", str);
assert(reply != NULL && reply->type == REDIS_REPLY_STATUS);
freeReplyObject(reply);
}
static int disconnect(redisContext *c, int keep_fd) { static int disconnect(redisContext *c, int keep_fd) {
redisReply *reply; redisReply *reply;
...@@ -105,9 +179,9 @@ static int disconnect(redisContext *c, int keep_fd) { ...@@ -105,9 +179,9 @@ static int disconnect(redisContext *c, int keep_fd) {
return -1; return -1;
} }
static void do_ssl_handshake(redisContext *c, struct config config) { static void do_ssl_handshake(redisContext *c) {
#ifdef HIREDIS_TEST_SSL #ifdef HIREDIS_TEST_SSL
redisSecureConnection(c, config.ssl.ca_cert, config.ssl.cert, config.ssl.key, NULL); redisInitiateSSLWithContext(c, _ssl_ctx);
if (c->err) { if (c->err) {
printf("SSL error: %s\n", c->errstr); printf("SSL error: %s\n", c->errstr);
redisFree(c); redisFree(c);
...@@ -115,7 +189,6 @@ static void do_ssl_handshake(redisContext *c, struct config config) { ...@@ -115,7 +189,6 @@ static void do_ssl_handshake(redisContext *c, struct config config) {
} }
#else #else
(void) c; (void) c;
(void) config;
#endif #endif
} }
...@@ -150,7 +223,7 @@ static redisContext *do_connect(struct config config) { ...@@ -150,7 +223,7 @@ static redisContext *do_connect(struct config config) {
} }
if (config.type == CONN_SSL) { if (config.type == CONN_SSL) {
do_ssl_handshake(c, config); do_ssl_handshake(c);
} }
return select_database(c); return select_database(c);
...@@ -160,7 +233,7 @@ static void do_reconnect(redisContext *c, struct config config) { ...@@ -160,7 +233,7 @@ static void do_reconnect(redisContext *c, struct config config) {
redisReconnect(c); redisReconnect(c);
if (config.type == CONN_SSL) { if (config.type == CONN_SSL) {
do_ssl_handshake(c, config); do_ssl_handshake(c);
} }
} }
...@@ -172,43 +245,43 @@ static void test_format_commands(void) { ...@@ -172,43 +245,43 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"SET foo bar"); len = redisFormatCommand(&cmd,"SET foo bar");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2)); len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd); hi_free(cmd);
test("Format command with %%s string interpolation: "); test("Format command with %%s string interpolation: ");
len = redisFormatCommand(&cmd,"SET %s %s","foo","bar"); len = redisFormatCommand(&cmd,"SET %s %s","foo","bar");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2)); len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd); hi_free(cmd);
test("Format command with %%s and an empty string: "); test("Format command with %%s and an empty string: ");
len = redisFormatCommand(&cmd,"SET %s %s","foo",""); len = redisFormatCommand(&cmd,"SET %s %s","foo","");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(0+2)); len == 4+4+(3+2)+4+(3+2)+4+(0+2));
free(cmd); hi_free(cmd);
test("Format command with an empty string in between proper interpolations: "); test("Format command with an empty string in between proper interpolations: ");
len = redisFormatCommand(&cmd,"SET %s %s","","foo"); len = redisFormatCommand(&cmd,"SET %s %s","","foo");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(0+2)+4+(3+2)); len == 4+4+(3+2)+4+(0+2)+4+(3+2));
free(cmd); hi_free(cmd);
test("Format command with %%b string interpolation: "); test("Format command with %%b string interpolation: ");
len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"b\0r",(size_t)3); len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"b\0r",(size_t)3);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2)); len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd); hi_free(cmd);
test("Format command with %%b and an empty string: "); test("Format command with %%b and an empty string: ");
len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"",(size_t)0); len = redisFormatCommand(&cmd,"SET %b %b","foo",(size_t)3,"",(size_t)0);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(0+2)); len == 4+4+(3+2)+4+(3+2)+4+(0+2));
free(cmd); hi_free(cmd);
test("Format command with literal %%: "); test("Format command with literal %%: ");
len = redisFormatCommand(&cmd,"SET %% %%"); len = redisFormatCommand(&cmd,"SET %% %%");
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(1+2)+4+(1+2)); len == 4+4+(3+2)+4+(1+2)+4+(1+2));
free(cmd); hi_free(cmd);
/* Vararg width depends on the type. These tests make sure that the /* Vararg width depends on the type. These tests make sure that the
* width is correctly determined using the format and subsequent varargs * width is correctly determined using the format and subsequent varargs
...@@ -219,7 +292,7 @@ static void test_format_commands(void) { ...@@ -219,7 +292,7 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \ len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \
test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \ test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \
len == 4+5+(12+2)+4+(9+2)); \ len == 4+5+(12+2)+4+(9+2)); \
free(cmd); \ hi_free(cmd); \
} while(0) } while(0)
#define FLOAT_WIDTH_TEST(type) do { \ #define FLOAT_WIDTH_TEST(type) do { \
...@@ -228,7 +301,7 @@ static void test_format_commands(void) { ...@@ -228,7 +301,7 @@ static void test_format_commands(void) {
len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \ len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \
test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \ test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \
len == 4+5+(12+2)+4+(9+2)); \ len == 4+5+(12+2)+4+(9+2)); \
free(cmd); \ hi_free(cmd); \
} while(0) } while(0)
INTEGER_WIDTH_TEST("d", int); INTEGER_WIDTH_TEST("d", int);
...@@ -259,24 +332,24 @@ static void test_format_commands(void) { ...@@ -259,24 +332,24 @@ static void test_format_commands(void) {
len = redisFormatCommandArgv(&cmd,argc,argv,NULL); len = redisFormatCommandArgv(&cmd,argc,argv,NULL);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2)); len == 4+4+(3+2)+4+(3+2)+4+(3+2));
free(cmd); hi_free(cmd);
test("Format command by passing argc/argv with lengths: "); test("Format command by passing argc/argv with lengths: ");
len = redisFormatCommandArgv(&cmd,argc,argv,lens); len = redisFormatCommandArgv(&cmd,argc,argv,lens);
test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(7+2)+4+(3+2)); len == 4+4+(3+2)+4+(7+2)+4+(3+2));
free(cmd); hi_free(cmd);
sds sds_cmd; sds sds_cmd;
sds_cmd = sdsempty(); sds_cmd = NULL;
test("Format command into sds by passing argc/argv without lengths: "); test("Format command into sds by passing argc/argv without lengths: ");
len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,NULL); len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,NULL);
test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 &&
len == 4+4+(3+2)+4+(3+2)+4+(3+2)); len == 4+4+(3+2)+4+(3+2)+4+(3+2));
sdsfree(sds_cmd); sdsfree(sds_cmd);
sds_cmd = sdsempty(); sds_cmd = NULL;
test("Format command into sds by passing argc/argv with lengths: "); test("Format command into sds by passing argc/argv with lengths: ");
len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,lens); len = redisFormatSdsCommandArgv(&sds_cmd,argc,argv,lens);
test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && test_cond(strncmp(sds_cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 &&
...@@ -300,7 +373,7 @@ static void test_append_formatted_commands(struct config config) { ...@@ -300,7 +373,7 @@ static void test_append_formatted_commands(struct config config) {
assert(redisGetReply(c, (void*)&reply) == REDIS_OK); assert(redisGetReply(c, (void*)&reply) == REDIS_OK);
free(cmd); hi_free(cmd);
freeReplyObject(reply); freeReplyObject(reply);
disconnect(c, 0); disconnect(c, 0);
...@@ -308,7 +381,7 @@ static void test_append_formatted_commands(struct config config) { ...@@ -308,7 +381,7 @@ static void test_append_formatted_commands(struct config config) {
static void test_reply_reader(void) { static void test_reply_reader(void) {
redisReader *reader; redisReader *reader;
void *reply; void *reply, *root;
int ret; int ret;
int i; int i;
...@@ -332,16 +405,26 @@ static void test_reply_reader(void) { ...@@ -332,16 +405,26 @@ static void test_reply_reader(void) {
strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0);
redisReaderFree(reader); redisReaderFree(reader);
test("Set error on nested multi bulks with depth > 7: ");
reader = redisReaderCreate(); reader = redisReaderCreate();
test("Can handle arbitrarily nested multi-bulks: ");
for (i = 0; i < 9; i++) { for (i = 0; i < 128; i++) {
redisReaderFeed(reader,(char*)"*1\r\n",4); redisReaderFeed(reader,(char*)"*1\r\n", 4);
} }
redisReaderFeed(reader,(char*)"$6\r\nLOLWUT\r\n",12);
ret = redisReaderGetReply(reader,&reply);
root = reply; /* Keep track of the root reply */
test_cond(ret == REDIS_OK &&
((redisReply*)reply)->type == REDIS_REPLY_ARRAY &&
((redisReply*)reply)->elements == 1);
ret = redisReaderGetReply(reader,NULL); test("Can parse arbitrarily nested multi-bulks correctly: ");
test_cond(ret == REDIS_ERR && while(i--) {
strncasecmp(reader->errstr,"No support for",14) == 0); assert(reply != NULL && ((redisReply*)reply)->type == REDIS_REPLY_ARRAY);
reply = ((redisReply*)reply)->element[0];
}
test_cond(((redisReply*)reply)->type == REDIS_REPLY_STRING &&
!memcmp(((redisReply*)reply)->str, "LOLWUT", 6));
freeReplyObject(root);
redisReaderFree(reader); redisReaderFree(reader);
test("Correctly parses LLONG_MAX: "); test("Correctly parses LLONG_MAX: ");
...@@ -400,6 +483,16 @@ static void test_reply_reader(void) { ...@@ -400,6 +483,16 @@ static void test_reply_reader(void) {
freeReplyObject(reply); freeReplyObject(reply);
redisReaderFree(reader); redisReaderFree(reader);
test("Can configure maximum multi-bulk elements: ");
reader = redisReaderCreate();
reader->maxelements = 1024;
redisReaderFeed(reader, "*1025\r\n", 7);
ret = redisReaderGetReply(reader,&reply);
test_cond(ret == REDIS_ERR &&
strcasecmp(reader->errstr, "Multi-bulk length out of range") == 0);
freeReplyObject(reply);
redisReaderFree(reader);
#if LLONG_MAX > SIZE_MAX #if LLONG_MAX > SIZE_MAX
test("Set error when array > SIZE_MAX: "); test("Set error when array > SIZE_MAX: ");
reader = redisReaderCreate(); reader = redisReaderCreate();
...@@ -459,6 +552,32 @@ static void test_reply_reader(void) { ...@@ -459,6 +552,32 @@ static void test_reply_reader(void) {
((redisReply*)reply)->elements == 0); ((redisReply*)reply)->elements == 0);
freeReplyObject(reply); freeReplyObject(reply);
redisReaderFree(reader); redisReaderFree(reader);
/* RESP3 verbatim strings (GitHub issue #802) */
test("Can parse RESP3 verbatim strings: ");
reader = redisReaderCreate();
redisReaderFeed(reader,(char*)"=10\r\ntxt:LOLWUT\r\n",17);
ret = redisReaderGetReply(reader,&reply);
test_cond(ret == REDIS_OK &&
((redisReply*)reply)->type == REDIS_REPLY_VERB &&
!memcmp(((redisReply*)reply)->str,"LOLWUT", 6));
freeReplyObject(reply);
redisReaderFree(reader);
/* RESP3 push messages (Github issue #815) */
test("Can parse RESP3 push messages: ");
reader = redisReaderCreate();
redisReaderFeed(reader,(char*)">2\r\n$6\r\nLOLWUT\r\n:42\r\n",21);
ret = redisReaderGetReply(reader,&reply);
test_cond(ret == REDIS_OK &&
((redisReply*)reply)->type == REDIS_REPLY_PUSH &&
((redisReply*)reply)->elements == 2 &&
((redisReply*)reply)->element[0]->type == REDIS_REPLY_STRING &&
!memcmp(((redisReply*)reply)->element[0]->str,"LOLWUT",6) &&
((redisReply*)reply)->element[1]->type == REDIS_REPLY_INTEGER &&
((redisReply*)reply)->element[1]->integer == 42);
freeReplyObject(reply);
redisReaderFree(reader);
} }
static void test_free_null(void) { static void test_free_null(void) {
...@@ -474,6 +593,47 @@ static void test_free_null(void) { ...@@ -474,6 +593,47 @@ static void test_free_null(void) {
test_cond(reply == NULL); test_cond(reply == NULL);
} }
static void *hi_malloc_fail(size_t size) {
(void)size;
return NULL;
}
static void *hi_calloc_fail(size_t nmemb, size_t size) {
(void)nmemb;
(void)size;
return NULL;
}
static void *hi_realloc_fail(void *ptr, size_t size) {
(void)ptr;
(void)size;
return NULL;
}
static void test_allocator_injection(void) {
hiredisAllocFuncs ha = {
.mallocFn = hi_malloc_fail,
.callocFn = hi_calloc_fail,
.reallocFn = hi_realloc_fail,
.strdupFn = strdup,
.freeFn = free,
};
// Override hiredis allocators
hiredisSetAllocators(&ha);
test("redisContext uses injected allocators: ");
redisContext *c = redisConnect("localhost", 6379);
test_cond(c == NULL);
test("redisReader uses injected allocators: ");
redisReader *reader = redisReaderCreate();
test_cond(reader == NULL);
// Return allocators to default
hiredisResetAllocators();
}
#define HIREDIS_BAD_DOMAIN "idontexist-noreally.com" #define HIREDIS_BAD_DOMAIN "idontexist-noreally.com"
static void test_blocking_connection_errors(void) { static void test_blocking_connection_errors(void) {
redisContext *c; redisContext *c;
...@@ -491,19 +651,19 @@ static void test_blocking_connection_errors(void) { ...@@ -491,19 +651,19 @@ static void test_blocking_connection_errors(void) {
(strcmp(c->errstr, "Name or service not known") == 0 || (strcmp(c->errstr, "Name or service not known") == 0 ||
strcmp(c->errstr, "Can't resolve: " HIREDIS_BAD_DOMAIN) == 0 || strcmp(c->errstr, "Can't resolve: " HIREDIS_BAD_DOMAIN) == 0 ||
strcmp(c->errstr, "Name does not resolve") == 0 || strcmp(c->errstr, "Name does not resolve") == 0 ||
strcmp(c->errstr, strcmp(c->errstr, "nodename nor servname provided, or not known") == 0 ||
"nodename nor servname provided, or not known") == 0 ||
strcmp(c->errstr, "No address associated with hostname") == 0 || strcmp(c->errstr, "No address associated with hostname") == 0 ||
strcmp(c->errstr, "Temporary failure in name resolution") == 0 || strcmp(c->errstr, "Temporary failure in name resolution") == 0 ||
strcmp(c->errstr, strcmp(c->errstr, "hostname nor servname provided, or not known") == 0 ||
"hostname nor servname provided, or not known") == 0 || strcmp(c->errstr, "no address associated with name") == 0 ||
strcmp(c->errstr, "no address associated with name") == 0)); strcmp(c->errstr, "No such host is known. ") == 0));
redisFree(c); redisFree(c);
} else { } else {
printf("Skipping NXDOMAIN test. Found evil ISP!\n"); printf("Skipping NXDOMAIN test. Found evil ISP!\n");
freeaddrinfo(ai_tmp); freeaddrinfo(ai_tmp);
} }
#ifndef _WIN32
test("Returns error when the port is not open: "); test("Returns error when the port is not open: ");
c = redisConnect((char*)"localhost", 1); c = redisConnect((char*)"localhost", 1);
test_cond(c->err == REDIS_ERR_IO && test_cond(c->err == REDIS_ERR_IO &&
...@@ -514,11 +674,147 @@ static void test_blocking_connection_errors(void) { ...@@ -514,11 +674,147 @@ static void test_blocking_connection_errors(void) {
c = redisConnectUnix((char*)"/tmp/idontexist.sock"); c = redisConnectUnix((char*)"/tmp/idontexist.sock");
test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */ test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */
redisFree(c); redisFree(c);
#endif
}
/* Dummy push handler */
void push_handler(void *privdata, void *reply) {
int *counter = privdata;
freeReplyObject(reply);
*counter += 1;
}
/* Dummy function just to test setting a callback with redisOptions */
void push_handler_async(redisAsyncContext *ac, void *reply) {
(void)ac;
(void)reply;
}
static void test_resp3_push_handler(redisContext *c) {
redisPushFn *old = NULL;
redisReply *reply;
void *privdata;
int n = 0;
/* Switch to RESP3 and turn on client tracking */
send_hello(c, 3);
send_client_tracking(c, "ON");
privdata = c->privdata;
c->privdata = &n;
reply = redisCommand(c, "GET key:0");
assert(reply != NULL);
freeReplyObject(reply);
test("RESP3 PUSH messages are handled out of band by default: ");
reply = redisCommand(c, "SET key:0 val:0");
test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS);
freeReplyObject(reply);
assert((reply = redisCommand(c, "GET key:0")) != NULL);
freeReplyObject(reply);
old = redisSetPushCallback(c, push_handler);
test("We can set a custom RESP3 PUSH handler: ");
reply = redisCommand(c, "SET key:0 val:0");
test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS && n == 1);
freeReplyObject(reply);
/* Unset the push callback and generate an invalidate message making
* sure it is not handled out of band. */
test("With no handler, PUSH replies come in-band: ");
redisSetPushCallback(c, NULL);
assert((reply = redisCommand(c, "GET key:0")) != NULL);
freeReplyObject(reply);
assert((reply = redisCommand(c, "SET key:0 invalid")) != NULL);
test_cond(reply->type == REDIS_REPLY_PUSH);
freeReplyObject(reply);
test("With no PUSH handler, no replies are lost: ");
assert(redisGetReply(c, (void**)&reply) == REDIS_OK);
test_cond(reply != NULL && reply->type == REDIS_REPLY_STATUS);
freeReplyObject(reply);
/* Return to the originally set PUSH handler */
assert(old != NULL);
redisSetPushCallback(c, old);
/* Switch back to RESP2 and disable tracking */
c->privdata = privdata;
send_client_tracking(c, "OFF");
send_hello(c, 2);
}
redisOptions get_redis_tcp_options(struct config config) {
redisOptions options = {0};
REDIS_OPTIONS_SET_TCP(&options, config.tcp.host, config.tcp.port);
return options;
}
static void test_resp3_push_options(struct config config) {
redisAsyncContext *ac;
redisContext *c;
redisOptions options;
test("We set a default RESP3 handler for redisContext: ");
options = get_redis_tcp_options(config);
assert((c = redisConnectWithOptions(&options)) != NULL);
test_cond(c->push_cb != NULL);
redisFree(c);
test("We don't set a default RESP3 push handler for redisAsyncContext: ");
options = get_redis_tcp_options(config);
assert((ac = redisAsyncConnectWithOptions(&options)) != NULL);
test_cond(ac->c.push_cb == NULL);
redisAsyncFree(ac);
test("Our REDIS_OPT_NO_PUSH_AUTOFREE flag works: ");
options = get_redis_tcp_options(config);
options.options |= REDIS_OPT_NO_PUSH_AUTOFREE;
assert((c = redisConnectWithOptions(&options)) != NULL);
test_cond(c->push_cb == NULL);
redisFree(c);
test("We can use redisOptions to set a custom PUSH handler for redisContext: ");
options = get_redis_tcp_options(config);
options.push_cb = push_handler;
assert((c = redisConnectWithOptions(&options)) != NULL);
test_cond(c->push_cb == push_handler);
redisFree(c);
test("We can use redisOptions to set a custom PUSH handler for redisAsyncContext: ");
options = get_redis_tcp_options(config);
options.async_push_cb = push_handler_async;
assert((ac = redisAsyncConnectWithOptions(&options)) != NULL);
test_cond(ac->push_cb == push_handler_async);
redisAsyncFree(ac);
}
void free_privdata(void *privdata) {
struct privdata *data = privdata;
data->dtor_counter++;
}
static void test_privdata_hooks(struct config config) {
struct privdata data = {0};
redisOptions options;
redisContext *c;
test("We can use redisOptions to set privdata: ");
options = get_redis_tcp_options(config);
REDIS_OPTIONS_SET_PRIVDATA(&options, &data, free_privdata);
assert((c = redisConnectWithOptions(&options)) != NULL);
test_cond(c->privdata == &data);
test("Our privdata destructor fires when we free the context: ");
redisFree(c);
test_cond(data.dtor_counter == 1);
} }
static void test_blocking_connection(struct config config) { static void test_blocking_connection(struct config config) {
redisContext *c; redisContext *c;
redisReply *reply; redisReply *reply;
int major;
c = do_connect(config); c = do_connect(config);
...@@ -591,14 +887,42 @@ static void test_blocking_connection(struct config config) { ...@@ -591,14 +887,42 @@ static void test_blocking_connection(struct config config) {
strcasecmp(reply->element[1]->str,"pong") == 0); strcasecmp(reply->element[1]->str,"pong") == 0);
freeReplyObject(reply); freeReplyObject(reply);
/* Make sure passing NULL to redisGetReply is safe */
test("Can pass NULL to redisGetReply: ");
assert(redisAppendCommand(c, "PING") == REDIS_OK);
test_cond(redisGetReply(c, NULL) == REDIS_OK);
get_redis_version(c, &major, NULL);
if (major >= 6) test_resp3_push_handler(c);
test_resp3_push_options(config);
test_privdata_hooks(config);
disconnect(c, 0); disconnect(c, 0);
} }
/* Send DEBUG SLEEP 0 to detect if we have this command */
static int detect_debug_sleep(redisContext *c) {
int detected;
redisReply *reply = redisCommand(c, "DEBUG SLEEP 0\r\n");
if (reply == NULL || c->err) {
const char *cause = c->err ? c->errstr : "(none)";
fprintf(stderr, "Error testing for DEBUG SLEEP (Redis error: %s), exiting\n", cause);
exit(-1);
}
detected = reply->type == REDIS_REPLY_STATUS;
freeReplyObject(reply);
return detected;
}
static void test_blocking_connection_timeouts(struct config config) { static void test_blocking_connection_timeouts(struct config config) {
redisContext *c; redisContext *c;
redisReply *reply; redisReply *reply;
ssize_t s; ssize_t s;
const char *cmd = "DEBUG SLEEP 3\r\n"; const char *sleep_cmd = "DEBUG SLEEP 3\r\n";
struct timeval tv; struct timeval tv;
c = do_connect(config); c = do_connect(config);
...@@ -615,14 +939,24 @@ static void test_blocking_connection_timeouts(struct config config) { ...@@ -615,14 +939,24 @@ static void test_blocking_connection_timeouts(struct config config) {
c = do_connect(config); c = do_connect(config);
test("Does not return a reply when the command times out: "); test("Does not return a reply when the command times out: ");
redisAppendFormattedCommand(c, cmd, strlen(cmd)); if (detect_debug_sleep(c)) {
s = c->funcs->write(c); redisAppendFormattedCommand(c, sleep_cmd, strlen(sleep_cmd));
tv.tv_sec = 0; s = c->funcs->write(c);
tv.tv_usec = 10000; tv.tv_sec = 0;
redisSetTimeout(c, tv); tv.tv_usec = 10000;
reply = redisCommand(c, "GET foo"); redisSetTimeout(c, tv);
test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO && strcmp(c->errstr, "Resource temporarily unavailable") == 0); reply = redisCommand(c, "GET foo");
freeReplyObject(reply); #ifndef _WIN32
test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO &&
strcmp(c->errstr, "Resource temporarily unavailable") == 0);
#else
test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_TIMEOUT &&
strcmp(c->errstr, "recv timeout") == 0);
#endif
freeReplyObject(reply);
} else {
test_skipped();
}
test("Reconnect properly reconnects after a timeout: "); test("Reconnect properly reconnects after a timeout: ");
do_reconnect(c, config); do_reconnect(c, config);
...@@ -649,18 +983,7 @@ static void test_blocking_io_errors(struct config config) { ...@@ -649,18 +983,7 @@ static void test_blocking_io_errors(struct config config) {
/* Connect to target given by config. */ /* Connect to target given by config. */
c = do_connect(config); c = do_connect(config);
{ get_redis_version(c, &major, &minor);
/* Find out Redis version to determine the path for the next test */
const char *field = "redis_version:";
char *p, *eptr;
reply = redisCommand(c,"INFO");
p = strstr(reply->str,field);
major = strtol(p+strlen(field),&eptr,10);
p = eptr+1; /* char next to the first "." */
minor = strtol(p,&eptr,10);
freeReplyObject(reply);
}
test("Returns I/O error when the connection is lost: "); test("Returns I/O error when the connection is lost: ");
reply = redisCommand(c,"QUIT"); reply = redisCommand(c,"QUIT");
...@@ -674,6 +997,7 @@ static void test_blocking_io_errors(struct config config) { ...@@ -674,6 +997,7 @@ static void test_blocking_io_errors(struct config config) {
test_cond(reply == NULL); test_cond(reply == NULL);
} }
#ifndef _WIN32
/* On 2.0, QUIT will cause the connection to be closed immediately and /* On 2.0, QUIT will cause the connection to be closed immediately and
* the read(2) for the reply on QUIT will set the error to EOF. * the read(2) for the reply on QUIT will set the error to EOF.
* On >2.0, QUIT will return with OK and another read(2) needed to be * On >2.0, QUIT will return with OK and another read(2) needed to be
...@@ -681,14 +1005,19 @@ static void test_blocking_io_errors(struct config config) { ...@@ -681,14 +1005,19 @@ static void test_blocking_io_errors(struct config config) {
* conditions, the error will be set to EOF. */ * conditions, the error will be set to EOF. */
assert(c->err == REDIS_ERR_EOF && assert(c->err == REDIS_ERR_EOF &&
strcmp(c->errstr,"Server closed the connection") == 0); strcmp(c->errstr,"Server closed the connection") == 0);
#endif
redisFree(c); redisFree(c);
c = do_connect(config); c = do_connect(config);
test("Returns I/O error on socket timeout: "); test("Returns I/O error on socket timeout: ");
struct timeval tv = { 0, 1000 }; struct timeval tv = { 0, 1000 };
assert(redisSetTimeout(c,tv) == REDIS_OK); assert(redisSetTimeout(c,tv) == REDIS_OK);
test_cond(redisGetReply(c,&_reply) == REDIS_ERR && int respcode = redisGetReply(c,&_reply);
c->err == REDIS_ERR_IO && errno == EAGAIN); #ifndef _WIN32
test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_IO && errno == EAGAIN);
#else
test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_TIMEOUT);
#endif
redisFree(c); redisFree(c);
} }
...@@ -716,6 +1045,18 @@ static void test_invalid_timeout_errors(struct config config) { ...@@ -716,6 +1045,18 @@ static void test_invalid_timeout_errors(struct config config) {
redisFree(c); redisFree(c);
} }
/* Wrap malloc to abort on failure so OOM checks don't make the test logic
* harder to follow. */
void *hi_malloc_safe(size_t size) {
void *ptr = hi_malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Error: Out of memory\n");
exit(-1);
}
return ptr;
}
static void test_throughput(struct config config) { static void test_throughput(struct config config) {
redisContext *c = do_connect(config); redisContext *c = do_connect(config);
redisReply **replies; redisReply **replies;
...@@ -727,7 +1068,7 @@ static void test_throughput(struct config config) { ...@@ -727,7 +1068,7 @@ static void test_throughput(struct config config) {
freeReplyObject(redisCommand(c,"LPUSH mylist foo")); freeReplyObject(redisCommand(c,"LPUSH mylist foo"));
num = 1000; num = 1000;
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec(); t1 = usec();
for (i = 0; i < num; i++) { for (i = 0; i < num; i++) {
replies[i] = redisCommand(c,"PING"); replies[i] = redisCommand(c,"PING");
...@@ -735,10 +1076,10 @@ static void test_throughput(struct config config) { ...@@ -735,10 +1076,10 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec(); t1 = usec();
for (i = 0; i < num; i++) { for (i = 0; i < num; i++) {
replies[i] = redisCommand(c,"LRANGE mylist 0 499"); replies[i] = redisCommand(c,"LRANGE mylist 0 499");
...@@ -747,10 +1088,10 @@ static void test_throughput(struct config config) { ...@@ -747,10 +1088,10 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
t1 = usec(); t1 = usec();
for (i = 0; i < num; i++) { for (i = 0; i < num; i++) {
replies[i] = redisCommand(c, "INCRBY incrkey %d", 1000000); replies[i] = redisCommand(c, "INCRBY incrkey %d", 1000000);
...@@ -758,11 +1099,11 @@ static void test_throughput(struct config config) { ...@@ -758,11 +1099,11 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx INCRBY: %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx INCRBY: %.3fs)\n", num, (t2-t1)/1000000.0);
num = 10000; num = 10000;
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++) for (i = 0; i < num; i++)
redisAppendCommand(c,"PING"); redisAppendCommand(c,"PING");
t1 = usec(); t1 = usec();
...@@ -772,10 +1113,10 @@ static void test_throughput(struct config config) { ...@@ -772,10 +1113,10 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++) for (i = 0; i < num; i++)
redisAppendCommand(c,"LRANGE mylist 0 499"); redisAppendCommand(c,"LRANGE mylist 0 499");
t1 = usec(); t1 = usec();
...@@ -786,10 +1127,10 @@ static void test_throughput(struct config config) { ...@@ -786,10 +1127,10 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
replies = malloc(sizeof(redisReply*)*num); replies = hi_malloc_safe(sizeof(redisReply*)*num);
for (i = 0; i < num; i++) for (i = 0; i < num; i++)
redisAppendCommand(c,"INCRBY incrkey %d", 1000000); redisAppendCommand(c,"INCRBY incrkey %d", 1000000);
t1 = usec(); t1 = usec();
...@@ -799,7 +1140,7 @@ static void test_throughput(struct config config) { ...@@ -799,7 +1140,7 @@ static void test_throughput(struct config config) {
} }
t2 = usec(); t2 = usec();
for (i = 0; i < num; i++) freeReplyObject(replies[i]); for (i = 0; i < num; i++) freeReplyObject(replies[i]);
free(replies); hi_free(replies);
printf("\t(%dx INCRBY (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); printf("\t(%dx INCRBY (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0);
disconnect(c, 0); disconnect(c, 0);
...@@ -916,9 +1257,8 @@ int main(int argc, char **argv) { ...@@ -916,9 +1257,8 @@ int main(int argc, char **argv) {
}; };
int throughput = 1; int throughput = 1;
int test_inherit_fd = 1; int test_inherit_fd = 1;
int skips_as_fails = 0;
/* Ignore broken pipe signal (for I/O error tests). */ int test_unix_socket;
signal(SIGPIPE, SIG_IGN);
/* Parse command line options. */ /* Parse command line options. */
argv++; argc--; argv++; argc--;
...@@ -936,6 +1276,8 @@ int main(int argc, char **argv) { ...@@ -936,6 +1276,8 @@ int main(int argc, char **argv) {
throughput = 0; throughput = 0;
} else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) { } else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) {
test_inherit_fd = 0; test_inherit_fd = 0;
} else if (argc >= 1 && !strcmp(argv[0],"--skips-as-fails")) {
skips_as_fails = 1;
#ifdef HIREDIS_TEST_SSL #ifdef HIREDIS_TEST_SSL
} else if (argc >= 2 && !strcmp(argv[0],"--ssl-port")) { } else if (argc >= 2 && !strcmp(argv[0],"--ssl-port")) {
argv++; argc--; argv++; argc--;
...@@ -960,6 +1302,19 @@ int main(int argc, char **argv) { ...@@ -960,6 +1302,19 @@ int main(int argc, char **argv) {
argv++; argc--; argv++; argc--;
} }
#ifndef _WIN32
/* Ignore broken pipe signal (for I/O error tests). */
signal(SIGPIPE, SIG_IGN);
test_unix_socket = access(cfg.unix_sock.path, F_OK) == 0;
#else
/* Unix sockets don't exist in Windows */
test_unix_socket = 0;
#endif
test_allocator_injection();
test_format_commands(); test_format_commands();
test_reply_reader(); test_reply_reader();
test_blocking_connection_errors(); test_blocking_connection_errors();
...@@ -974,15 +1329,25 @@ int main(int argc, char **argv) { ...@@ -974,15 +1329,25 @@ int main(int argc, char **argv) {
test_append_formatted_commands(cfg); test_append_formatted_commands(cfg);
if (throughput) test_throughput(cfg); if (throughput) test_throughput(cfg);
printf("\nTesting against Unix socket connection (%s):\n", cfg.unix_sock.path); printf("\nTesting against Unix socket connection (%s): ", cfg.unix_sock.path);
cfg.type = CONN_UNIX; if (test_unix_socket) {
test_blocking_connection(cfg); printf("\n");
test_blocking_connection_timeouts(cfg); cfg.type = CONN_UNIX;
test_blocking_io_errors(cfg); test_blocking_connection(cfg);
if (throughput) test_throughput(cfg); test_blocking_connection_timeouts(cfg);
test_blocking_io_errors(cfg);
if (throughput) test_throughput(cfg);
} else {
test_skipped();
}
#ifdef HIREDIS_TEST_SSL #ifdef HIREDIS_TEST_SSL
if (cfg.ssl.port && cfg.ssl.host) { if (cfg.ssl.port && cfg.ssl.host) {
redisInitOpenSSL();
_ssl_ctx = redisCreateSSLContext(cfg.ssl.ca_cert, NULL, cfg.ssl.cert, cfg.ssl.key, NULL, NULL);
assert(_ssl_ctx != NULL);
printf("\nTesting against SSL connection (%s:%d):\n", cfg.ssl.host, cfg.ssl.port); printf("\nTesting against SSL connection (%s:%d):\n", cfg.ssl.host, cfg.ssl.port);
cfg.type = CONN_SSL; cfg.type = CONN_SSL;
...@@ -992,21 +1357,31 @@ int main(int argc, char **argv) { ...@@ -992,21 +1357,31 @@ int main(int argc, char **argv) {
test_invalid_timeout_errors(cfg); test_invalid_timeout_errors(cfg);
test_append_formatted_commands(cfg); test_append_formatted_commands(cfg);
if (throughput) test_throughput(cfg); if (throughput) test_throughput(cfg);
redisFreeSSLContext(_ssl_ctx);
_ssl_ctx = NULL;
} }
#endif #endif
if (test_inherit_fd) { if (test_inherit_fd) {
printf("\nTesting against inherited fd (%s):\n", cfg.unix_sock.path); printf("\nTesting against inherited fd (%s): ", cfg.unix_sock.path);
cfg.type = CONN_FD; if (test_unix_socket) {
test_blocking_connection(cfg); printf("\n");
cfg.type = CONN_FD;
test_blocking_connection(cfg);
} else {
test_skipped();
}
} }
if (fails || (skips_as_fails && skips)) {
if (fails) {
printf("*** %d TESTS FAILED ***\n", fails); printf("*** %d TESTS FAILED ***\n", fails);
if (skips) {
printf("*** %d TESTS SKIPPED ***\n", skips);
}
return 1; return 1;
} }
printf("ALL TESTS PASSED\n"); printf("ALL TESTS PASSED (%d skipped)\n", skips);
return 0; return 0;
} }
...@@ -4,7 +4,9 @@ REDIS_SERVER=${REDIS_SERVER:-redis-server} ...@@ -4,7 +4,9 @@ REDIS_SERVER=${REDIS_SERVER:-redis-server}
REDIS_PORT=${REDIS_PORT:-56379} REDIS_PORT=${REDIS_PORT:-56379}
REDIS_SSL_PORT=${REDIS_SSL_PORT:-56443} REDIS_SSL_PORT=${REDIS_SSL_PORT:-56443}
TEST_SSL=${TEST_SSL:-0} TEST_SSL=${TEST_SSL:-0}
SKIPS_AS_FAILS=${SKIPS_AS_FAILS-:0}
SSL_TEST_ARGS= SSL_TEST_ARGS=
SKIPS_ARG=
tmpdir=$(mktemp -d) tmpdir=$(mktemp -d)
PID_FILE=${tmpdir}/hiredis-test-redis.pid PID_FILE=${tmpdir}/hiredis-test-redis.pid
...@@ -67,4 +69,10 @@ fi ...@@ -67,4 +69,10 @@ fi
cat ${tmpdir}/redis.conf cat ${tmpdir}/redis.conf
${REDIS_SERVER} ${tmpdir}/redis.conf ${REDIS_SERVER} ${tmpdir}/redis.conf
${TEST_PREFIX:-} ./hiredis-test -h 127.0.0.1 -p ${REDIS_PORT} -s ${SOCK_FILE} ${SSL_TEST_ARGS} # Wait until we detect the unix socket
while [ ! -S "${SOCK_FILE}" ]; do sleep 1; done
# Treat skips as failures if directed
[ "$SKIPS_AS_FAILS" = 1 ] && SKIPS_ARG="--skips-as-fails"
${TEST_PREFIX:-} ./hiredis-test -h 127.0.0.1 -p ${REDIS_PORT} -s ${SOCK_FILE} ${SSL_TEST_ARGS} ${SKIPS_ARG}
...@@ -234,6 +234,7 @@ static struct config { ...@@ -234,6 +234,7 @@ static struct config {
int askpass; int askpass;
char *user; char *user;
int output; /* output mode, see OUTPUT_* defines */ int output; /* output mode, see OUTPUT_* defines */
int push_output; /* Should we display spontaneous PUSH replies */
sds mb_delim; sds mb_delim;
char prompt[128]; char prompt[128];
char *eval; char *eval;
...@@ -267,6 +268,8 @@ static long getLongInfoField(char *info, char *field); ...@@ -267,6 +268,8 @@ static long getLongInfoField(char *info, char *field);
* Utility functions * Utility functions
*--------------------------------------------------------------------------- */ *--------------------------------------------------------------------------- */
static void cliPushHandler(void *, void *);
uint16_t crc16(const char *buf, int len); uint16_t crc16(const char *buf, int len);
static long long ustime(void) { static long long ustime(void) {
...@@ -873,8 +876,8 @@ static int cliConnect(int flags) { ...@@ -873,8 +876,8 @@ static int cliConnect(int flags) {
const char *err = NULL; const char *err = NULL;
if (cliSecureConnection(context, &err) == REDIS_ERR && err) { if (cliSecureConnection(context, &err) == REDIS_ERR && err) {
fprintf(stderr, "Could not negotiate a TLS connection: %s\n", err); fprintf(stderr, "Could not negotiate a TLS connection: %s\n", err);
context = NULL;
redisFree(context); redisFree(context);
context = NULL;
return REDIS_ERR; return REDIS_ERR;
} }
} }
...@@ -909,6 +912,12 @@ static int cliConnect(int flags) { ...@@ -909,6 +912,12 @@ static int cliConnect(int flags) {
if (cliSwitchProto() != REDIS_OK) if (cliSwitchProto() != REDIS_OK)
return REDIS_ERR; return REDIS_ERR;
} }
/* Set a PUSH handler if configured to do so. */
if (config.push_output) {
redisSetPushCallback(context, cliPushHandler);
}
return REDIS_OK; return REDIS_OK;
} }
...@@ -917,6 +926,31 @@ static void cliPrintContextError(void) { ...@@ -917,6 +926,31 @@ static void cliPrintContextError(void) {
fprintf(stderr,"Error: %s\n",context->errstr); fprintf(stderr,"Error: %s\n",context->errstr);
} }
static int isInvalidateReply(redisReply *reply) {
return reply->type == REDIS_REPLY_PUSH && reply->elements == 2 &&
reply->element[0]->type == REDIS_REPLY_STRING &&
!strncmp(reply->element[0]->str, "invalidate", 10) &&
reply->element[1]->type == REDIS_REPLY_ARRAY;
}
/* Special display handler for RESP3 'invalidate' messages.
* This function does not validate the reply, so it should
* already be confirmed correct */
static sds cliFormatInvalidateTTY(redisReply *r) {
sds out = sdsnew("-> invalidate: ");
for (size_t i = 0; i < r->element[1]->elements; i++) {
redisReply *key = r->element[1]->element[i];
assert(key->type == REDIS_REPLY_STRING);
out = sdscatfmt(out, "'%s'", key->str, key->len);
if (i < r->element[1]->elements - 1)
out = sdscatlen(out, ", ", 2);
}
return sdscatlen(out, "\n", 1);
}
static sds cliFormatReplyTTY(redisReply *r, char *prefix) { static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty(); sds out = sdsempty();
switch (r->type) { switch (r->type) {
...@@ -955,6 +989,7 @@ static sds cliFormatReplyTTY(redisReply *r, char *prefix) { ...@@ -955,6 +989,7 @@ static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
case REDIS_REPLY_ARRAY: case REDIS_REPLY_ARRAY:
case REDIS_REPLY_MAP: case REDIS_REPLY_MAP:
case REDIS_REPLY_SET: case REDIS_REPLY_SET:
case REDIS_REPLY_PUSH:
if (r->elements == 0) { if (r->elements == 0) {
if (r->type == REDIS_REPLY_ARRAY) if (r->type == REDIS_REPLY_ARRAY)
out = sdscat(out,"(empty array)\n"); out = sdscat(out,"(empty array)\n");
...@@ -962,6 +997,8 @@ static sds cliFormatReplyTTY(redisReply *r, char *prefix) { ...@@ -962,6 +997,8 @@ static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
out = sdscat(out,"(empty hash)\n"); out = sdscat(out,"(empty hash)\n");
else if (r->type == REDIS_REPLY_SET) else if (r->type == REDIS_REPLY_SET)
out = sdscat(out,"(empty set)\n"); out = sdscat(out,"(empty set)\n");
else if (r->type == REDIS_REPLY_PUSH)
out = sdscat(out,"(empty push)\n");
else else
out = sdscat(out,"(empty aggregate type)\n"); out = sdscat(out,"(empty aggregate type)\n");
} else { } else {
...@@ -1113,6 +1150,7 @@ static sds cliFormatReplyRaw(redisReply *r) { ...@@ -1113,6 +1150,7 @@ static sds cliFormatReplyRaw(redisReply *r) {
out = sdscatprintf(out,"%s",r->str); out = sdscatprintf(out,"%s",r->str);
break; break;
case REDIS_REPLY_ARRAY: case REDIS_REPLY_ARRAY:
case REDIS_REPLY_PUSH:
for (i = 0; i < r->elements; i++) { for (i = 0; i < r->elements; i++) {
if (i > 0) out = sdscat(out,config.mb_delim); if (i > 0) out = sdscat(out,config.mb_delim);
tmp = cliFormatReplyRaw(r->element[i]); tmp = cliFormatReplyRaw(r->element[i]);
...@@ -1169,6 +1207,7 @@ static sds cliFormatReplyCSV(redisReply *r) { ...@@ -1169,6 +1207,7 @@ static sds cliFormatReplyCSV(redisReply *r) {
out = sdscat(out,r->integer ? "true" : "false"); out = sdscat(out,r->integer ? "true" : "false");
break; break;
case REDIS_REPLY_ARRAY: case REDIS_REPLY_ARRAY:
case REDIS_REPLY_PUSH:
case REDIS_REPLY_MAP: /* CSV has no map type, just output flat list. */ case REDIS_REPLY_MAP: /* CSV has no map type, just output flat list. */
for (i = 0; i < r->elements; i++) { for (i = 0; i < r->elements; i++) {
sds tmp = cliFormatReplyCSV(r->element[i]); sds tmp = cliFormatReplyCSV(r->element[i]);
...@@ -1184,6 +1223,45 @@ static sds cliFormatReplyCSV(redisReply *r) { ...@@ -1184,6 +1223,45 @@ static sds cliFormatReplyCSV(redisReply *r) {
return out; return out;
} }
/* Generate reply strings in various output modes */
static sds cliFormatReply(redisReply *reply, int mode, int verbatim) {
sds out;
if (verbatim) {
out = cliFormatReplyRaw(reply);
} else if (mode == OUTPUT_STANDARD) {
out = cliFormatReplyTTY(reply, "");
} else if (mode == OUTPUT_RAW) {
out = cliFormatReplyRaw(reply);
out = sdscatlen(out, "\n", 1);
} else if (mode == OUTPUT_CSV) {
out = cliFormatReplyCSV(reply);
out = sdscatlen(out, "\n", 1);
} else {
fprintf(stderr, "Error: Unknown output encoding %d\n", mode);
exit(1);
}
return out;
}
/* Output any spontaneous PUSH reply we receive */
static void cliPushHandler(void *privdata, void *reply) {
UNUSED(privdata);
sds out;
if (config.output == OUTPUT_STANDARD && isInvalidateReply(reply)) {
out = cliFormatInvalidateTTY(reply);
} else {
out = cliFormatReply(reply, config.output, 0);
}
fwrite(out, sdslen(out), 1, stdout);
freeReplyObject(reply);
sdsfree(out);
}
static int cliReadReply(int output_raw_strings) { static int cliReadReply(int output_raw_strings) {
void *_reply; void *_reply;
redisReply *reply; redisReply *reply;
...@@ -1244,19 +1322,7 @@ static int cliReadReply(int output_raw_strings) { ...@@ -1244,19 +1322,7 @@ static int cliReadReply(int output_raw_strings) {
} }
if (output) { if (output) {
if (output_raw_strings) { out = cliFormatReply(reply, config.output, output_raw_strings);
out = cliFormatReplyRaw(reply);
} else {
if (config.output == OUTPUT_RAW) {
out = cliFormatReplyRaw(reply);
out = sdscat(out,"\n");
} else if (config.output == OUTPUT_STANDARD) {
out = cliFormatReplyTTY(reply,"");
} else if (config.output == OUTPUT_CSV) {
out = cliFormatReplyCSV(reply);
out = sdscat(out,"\n");
}
}
fwrite(out,sdslen(out),1,stdout); fwrite(out,sdslen(out),1,stdout);
sdsfree(out); sdsfree(out);
} }
...@@ -1346,6 +1412,10 @@ static int cliSendCommand(int argc, char **argv, long repeat) { ...@@ -1346,6 +1412,10 @@ static int cliSendCommand(int argc, char **argv, long repeat) {
if (config.pubsub_mode) { if (config.pubsub_mode) {
if (config.output != OUTPUT_RAW) if (config.output != OUTPUT_RAW)
printf("Reading messages... (press Ctrl-C to quit)\n"); printf("Reading messages... (press Ctrl-C to quit)\n");
/* Unset our default PUSH handler so this works in RESP2/RESP3 */
redisSetPushCallback(context, NULL);
while (1) { while (1) {
if (cliReadReply(output_raw) != REDIS_OK) exit(1); if (cliReadReply(output_raw) != REDIS_OK) exit(1);
} }
...@@ -1626,6 +1696,16 @@ static int parseOptions(int argc, char **argv) { ...@@ -1626,6 +1696,16 @@ static int parseOptions(int argc, char **argv) {
exit(0); exit(0);
} else if (!strcmp(argv[i],"-3")) { } else if (!strcmp(argv[i],"-3")) {
config.resp3 = 1; config.resp3 = 1;
} else if (!strcmp(argv[i],"--show-pushes") && !lastarg) {
char *argval = argv[++i];
if (!strncasecmp(argval, "n", 1)) {
config.push_output = 0;
} else if (!strncasecmp(argval, "y", 1)) {
config.push_output = 1;
} else {
fprintf(stderr, "Unknown --show-pushes value '%s' "
"(valid: '[y]es', '[n]o')\n", argval);
}
} else if (CLUSTER_MANAGER_MODE() && argv[i][0] != '-') { } else if (CLUSTER_MANAGER_MODE() && argv[i][0] != '-') {
if (config.cluster_manager_command.argc == 0) { if (config.cluster_manager_command.argc == 0) {
int j = i + 1; int j = i + 1;
...@@ -1734,6 +1814,8 @@ static void usage(void) { ...@@ -1734,6 +1814,8 @@ static void usage(void) {
" not a tty).\n" " not a tty).\n"
" --no-raw Force formatted output even when STDOUT is not a tty.\n" " --no-raw Force formatted output even when STDOUT is not a tty.\n"
" --csv Output in CSV format.\n" " --csv Output in CSV format.\n"
" --show-pushes <yn> Whether to print RESP3 PUSH messages. Enabled by default when\n"
" STDOUT is a tty but can be overriden with --show-pushes no.\n"
" --stat Print rolling stats about server: mem, clients, ...\n" " --stat Print rolling stats about server: mem, clients, ...\n"
" --latency Enter a special mode continuously sampling latency.\n" " --latency Enter a special mode continuously sampling latency.\n"
" If you use this mode in an interactive session it runs\n" " If you use this mode in an interactive session it runs\n"
...@@ -8063,10 +8145,13 @@ int main(int argc, char **argv) { ...@@ -8063,10 +8145,13 @@ int main(int argc, char **argv) {
spectrum_palette = spectrum_palette_color; spectrum_palette = spectrum_palette_color;
spectrum_palette_size = spectrum_palette_color_size; spectrum_palette_size = spectrum_palette_color_size;
if (!isatty(fileno(stdout)) && (getenv("FAKETTY") == NULL)) if (!isatty(fileno(stdout)) && (getenv("FAKETTY") == NULL)) {
config.output = OUTPUT_RAW; config.output = OUTPUT_RAW;
else config.push_output = 0;
} else {
config.output = OUTPUT_STANDARD; config.output = OUTPUT_STANDARD;
config.push_output = 1;
}
config.mb_delim = sdsnew("\n"); config.mb_delim = sdsnew("\n");
firstarg = parseOptions(argc,argv); firstarg = parseOptions(argc,argv);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment