Skip to content

Commit

Permalink
Fix connect timeout in HiredisConnection#init_ssl
Browse files Browse the repository at this point in the history
Ref: redis/hiredis#1059

Unfortunately I had to patch hiredis to expose
wether the handshake was completed or not.
  • Loading branch information
byroot committed Apr 13, 2022
1 parent 4ec3e98 commit 13bd1a5
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 56 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ jobs:
bundle exec rake ci
env:
DRIVER: "${{ matrix.driver }}"
EXT_PEDANTIC: "1"
13 changes: 10 additions & 3 deletions ext/redis_client/hiredis/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@
raise "Building hiredis failed" unless success
end

$CFLAGS << " -I#{hiredis_dir} "
$LDFLAGS << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a -lssl -lcrypto "
$CFLAGS << " -O3 "
$CFLAGS << " -I#{hiredis_dir}"
$LDFLAGS << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a -lssl -lcrypto"
$CFLAGS << " -O3"
$CFLAGS << " -std=c99"

if ENV["EXT_PEDANTIC"]
$CFLAGS << " -Wall"
$CFLAGS << " -Werror"
$CFLAGS << " -Wextra"
$CFLAGS << " -Wpedantic"
end

create_makefile("redis_client/hiredis_connection")
else
File.write("Makefile", dummy_makefile($srcdir).join)
Expand Down
26 changes: 22 additions & 4 deletions ext/redis_client/hiredis/hiredis_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "ruby.h"
#include <errno.h>
#include <sys/socket.h>
#include <stdbool.h>
#include "hiredis.h"
#include "hiredis_ssl.h"

Expand Down Expand Up @@ -183,8 +184,8 @@ static void *reply_create_bool(const redisReadTask *task, int bval) {
reply_append(task, bval ? Qtrue : Qfalse);
// Qfalse == NULL, so we can't return Qfalse as it would be interpreted as out of memory error.
// So we return Qnil instead.
assert(Qfalse == NULL);
assert(Qnil != NULL);
RUBY_ASSERT((void *)Qfalse == NULL);
RUBY_ASSERT((void *)Qnil != NULL);
return (void*)(bval ? Qtrue : Qnil);
}

Expand Down Expand Up @@ -380,10 +381,9 @@ static int hiredis_wait_writable(int fd, const struct timeval *timeout, int *iss
struct timeval to;
struct timeval *toptr = NULL;

rb_fdset_t fds;

/* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure
* must be paired with a call to rb_fd_term to free it. */
rb_fdset_t fds;
rb_fd_init(&fds);
rb_fd_set(fd, &fds);

Expand Down Expand Up @@ -466,6 +466,24 @@ static VALUE hiredis_init_ssl(VALUE self, VALUE ssl_param) {
if (redisInitiateSSLWithContext(connection->context, ssl_context->context) != REDIS_OK) {
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
}

redisSSL *redis_ssl = redisGetSSLSocket(connection->context);

if (redis_ssl->wantRead) {
int readable = 0;
if (hiredis_wait_readable(connection->context->fd, &connection->connect_timeout, &readable) < 0) {
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
}
if (!readable) {
errno = EAGAIN;
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
}

if (redisInitiateSSLContinue(connection->context) != REDIS_OK) {
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
};
}

return Qtrue;
}

Expand Down
30 changes: 30 additions & 0 deletions ext/redis_client/hiredis/vendor/hiredis_ssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#ifndef __HIREDIS_SSL_H
#define __HIREDIS_SSL_H

#include <openssl/ssl.h>

#ifdef __cplusplus
extern "C" {
#endif
Expand All @@ -46,6 +48,29 @@ struct ssl_st;
*/
typedef struct redisSSLContext redisSSLContext;

/* The SSL connection context is attached to SSL/TLS connections as a privdata. */
typedef struct redisSSL {
/**
* OpenSSL SSL object.
*/
SSL *ssl;

/**
* SSL_write() requires to be called again with the same arguments it was
* previously called with in the event of an SSL_read/SSL_write situation
*/
size_t lastLen;

/** Whether the SSL layer requires read (possibly before a write) */
int wantRead;

/**
* Whether a write was requested prior to a read. If set, the write()
* should resume whenever a read takes place, if possible
*/
int pendingWrite;
} redisSSL;

/**
* Initialization errors that redisCreateSSLContext() may return.
*/
Expand Down Expand Up @@ -114,12 +139,17 @@ void redisFreeSSLContext(redisSSLContext *redis_ssl_ctx);

int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx);

int redisInitiateSSLContinue(redisContext *c);

/**
* Initiate SSL/TLS negotiation on a provided OpenSSL SSL object.
*/

int redisInitiateSSL(redisContext *c, struct ssl_st *ssl);


redisSSL *redisGetSSLSocket(redisContext *c);

#ifdef __cplusplus
}
#endif
Expand Down
96 changes: 57 additions & 39 deletions ext/redis_client/hiredis/vendor/ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,6 @@ struct redisSSLContext {
char *server_name;
};

/* The SSL connection context is attached to SSL/TLS connections as a privdata. */
typedef struct redisSSL {
/**
* OpenSSL SSL object.
*/
SSL *ssl;

/**
* SSL_write() requires to be called again with the same arguments it was
* previously called with in the event of an SSL_read/SSL_write situation
*/
size_t lastLen;

/** Whether the SSL layer requires read (possibly before a write) */
int wantRead;

/**
* Whether a write was requested prior to a read. If set, the write()
* should resume whenever a read takes place, if possible
*/
int pendingWrite;
} redisSSL;

/* Forward declaration */
redisContextFuncs redisContextSSLFuncs;

Expand Down Expand Up @@ -163,6 +140,22 @@ int redisInitOpenSSL(void)
return REDIS_OK;
}

static int maybeCheckWant(redisSSL *rssl, int rv) {
/**
* If the error is WANT_READ or WANT_WRITE, the appropriate flags are set
* and true is returned. False is returned otherwise
*/
if (rv == SSL_ERROR_WANT_READ) {
rssl->wantRead = 1;
return 1;
} else if (rv == SSL_ERROR_WANT_WRITE) {
rssl->pendingWrite = 1;
return 1;
} else {
return 0;
}
}

/**
* redisSSLContext helper context destruction.
*/
Expand Down Expand Up @@ -261,6 +254,42 @@ redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *
return NULL;
}

int redisInitiateSSLContinue(redisContext *c) {
if (!c->privctx) {
__redisSetError(c, REDIS_ERR_OTHER, "redisContext is not associated");
return REDIS_ERR;
}

redisSSL *rssl = (redisSSL *)c->privctx;
ERR_clear_error();
int rv = SSL_connect(rssl->ssl);
if (rv == 1) {
c->privctx = rssl;
return REDIS_OK;
}

rv = SSL_get_error(rssl->ssl, rv);
if (((c->flags & REDIS_BLOCK) == 0) &&
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
maybeCheckWant(rssl, rv);
c->privctx = rssl;
return REDIS_OK;
}

if (c->err == 0) {
char err[512];
if (rv == SSL_ERROR_SYSCALL)
snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",strerror(errno));
else {
unsigned long e = ERR_peek_last_error();
snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",
ERR_reason_error_string(e));
}
__redisSetError(c, REDIS_ERR_IO, err);
}
return REDIS_ERR;
}

/**
* SSL Connection initialization.
*/
Expand Down Expand Up @@ -295,6 +324,7 @@ static int redisSSLConnect(redisContext *c, SSL *ssl) {
rv = SSL_get_error(rssl->ssl, rv);
if (((c->flags & REDIS_BLOCK) == 0) &&
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
maybeCheckWant(rssl, rv);
c->privctx = rssl;
return REDIS_OK;
}
Expand All @@ -315,6 +345,10 @@ static int redisSSLConnect(redisContext *c, SSL *ssl) {
return REDIS_ERR;
}

redisSSL *redisGetSSLSocket(redisContext *c) {
return c->privctx;
}

/**
* A wrapper around redisSSLConnect() for users who manage their own context and
* create their own SSL object.
Expand Down Expand Up @@ -361,22 +395,6 @@ int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx)
return REDIS_ERR;
}

static int maybeCheckWant(redisSSL *rssl, int rv) {
/**
* If the error is WANT_READ or WANT_WRITE, the appropriate flags are set
* and true is returned. False is returned otherwise
*/
if (rv == SSL_ERROR_WANT_READ) {
rssl->wantRead = 1;
return 1;
} else if (rv == SSL_ERROR_WANT_WRITE) {
rssl->pendingWrite = 1;
return 1;
} else {
return 0;
}
}

/**
* Implementation of redisContextFuncs for SSL connections.
*/
Expand Down
10 changes: 0 additions & 10 deletions test/redis_client/connection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,6 @@ class SSLConnectionTest < Minitest::Test
include ClientTestHelper
include ConnectionTests

if ENV["DRIVER"] == "hiredis"
def test_tcp_connect_downstream_timeout
skip "TODO: Find the proper way to timeout SSL connections with hiredis"
end

def test_tcp_connect_upstream_timeout
skip "TODO: Find the proper way to timeout SSL connections with hiredis"
end
end

private

def new_client(**overrides)
Expand Down

0 comments on commit 13bd1a5

Please sign in to comment.