Skip to content

Commit d368e9c

Browse files
committed
Fix connect timeout in HiredisConnection#init_ssl
Ref: redis/hiredis#1059 Unfortunately I had to patch hiredis to expose wether the handshake was completed or not.
1 parent 4ec3e98 commit d368e9c

File tree

6 files changed

+121
-57
lines changed

6 files changed

+121
-57
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ jobs:
5151
bundle exec rake ci
5252
env:
5353
DRIVER: "${{ matrix.driver }}"
54+
EXT_PEDANTIC: "1"

ext/redis_client/hiredis/extconf.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@
3636
raise "Building hiredis failed" unless success
3737
end
3838

39-
$CFLAGS << " -I#{hiredis_dir} "
40-
$LDFLAGS << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a -lssl -lcrypto "
41-
$CFLAGS << " -O3 "
42-
$CFLAGS << " -std=c99"
39+
$CFLAGS << " -I#{hiredis_dir}"
40+
$LDFLAGS << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a -lssl -lcrypto"
41+
$CFLAGS << " -O3"
42+
$CFLAGS << " -std=c99 "
43+
44+
if ENV["EXT_PEDANTIC"]
45+
$CFLAGS << " -Wall"
46+
$CFLAGS << " -Werror"
47+
$CFLAGS << " -Wextra"
48+
$CFLAGS << " -Wpedantic"
49+
end
4350

4451
create_makefile("redis_client/hiredis_connection")
4552
else

ext/redis_client/hiredis/hiredis_connection.c

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "ruby.h"
3434
#include <errno.h>
3535
#include <sys/socket.h>
36+
#include <stdbool.h>
3637
#include "hiredis.h"
3738
#include "hiredis_ssl.h"
3839

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

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

383-
rb_fdset_t fds;
384-
385384
/* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure
386385
* must be paired with a call to rb_fd_term to free it. */
386+
rb_fdset_t fds;
387387
rb_fd_init(&fds);
388388
rb_fd_set(fd, &fds);
389389

@@ -466,6 +466,24 @@ static VALUE hiredis_init_ssl(VALUE self, VALUE ssl_param) {
466466
if (redisInitiateSSLWithContext(connection->context, ssl_context->context) != REDIS_OK) {
467467
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
468468
}
469+
470+
redisSSL *redis_ssl = redisGetSSLSocket(connection->context);
471+
472+
if (redis_ssl->wantRead) {
473+
int readable = 0;
474+
if (hiredis_wait_readable(connection->context->fd, &connection->connect_timeout, &readable) < 0) {
475+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
476+
}
477+
if (!readable) {
478+
errno = EAGAIN;
479+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
480+
}
481+
482+
if (redisInitiateSSLContinue(connection->context) != REDIS_OK) {
483+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
484+
};
485+
}
486+
469487
return Qtrue;
470488
}
471489

ext/redis_client/hiredis/vendor/hiredis_ssl.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
#ifndef __HIREDIS_SSL_H
3333
#define __HIREDIS_SSL_H
3434

35+
#include <openssl/ssl.h>
36+
3537
#ifdef __cplusplus
3638
extern "C" {
3739
#endif
@@ -46,6 +48,29 @@ struct ssl_st;
4648
*/
4749
typedef struct redisSSLContext redisSSLContext;
4850

51+
/* The SSL connection context is attached to SSL/TLS connections as a privdata. */
52+
typedef struct redisSSL {
53+
/**
54+
* OpenSSL SSL object.
55+
*/
56+
SSL *ssl;
57+
58+
/**
59+
* SSL_write() requires to be called again with the same arguments it was
60+
* previously called with in the event of an SSL_read/SSL_write situation
61+
*/
62+
size_t lastLen;
63+
64+
/** Whether the SSL layer requires read (possibly before a write) */
65+
int wantRead;
66+
67+
/**
68+
* Whether a write was requested prior to a read. If set, the write()
69+
* should resume whenever a read takes place, if possible
70+
*/
71+
int pendingWrite;
72+
} redisSSL;
73+
4974
/**
5075
* Initialization errors that redisCreateSSLContext() may return.
5176
*/
@@ -114,12 +139,17 @@ void redisFreeSSLContext(redisSSLContext *redis_ssl_ctx);
114139

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

142+
int redisInitiateSSLContinue(redisContext *c);
143+
117144
/**
118145
* Initiate SSL/TLS negotiation on a provided OpenSSL SSL object.
119146
*/
120147

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

150+
151+
redisSSL *redisGetSSLSocket(redisContext *c);
152+
123153
#ifdef __cplusplus
124154
}
125155
#endif

ext/redis_client/hiredis/vendor/ssl.c

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -59,29 +59,6 @@ struct redisSSLContext {
5959
char *server_name;
6060
};
6161

62-
/* The SSL connection context is attached to SSL/TLS connections as a privdata. */
63-
typedef struct redisSSL {
64-
/**
65-
* OpenSSL SSL object.
66-
*/
67-
SSL *ssl;
68-
69-
/**
70-
* SSL_write() requires to be called again with the same arguments it was
71-
* previously called with in the event of an SSL_read/SSL_write situation
72-
*/
73-
size_t lastLen;
74-
75-
/** Whether the SSL layer requires read (possibly before a write) */
76-
int wantRead;
77-
78-
/**
79-
* Whether a write was requested prior to a read. If set, the write()
80-
* should resume whenever a read takes place, if possible
81-
*/
82-
int pendingWrite;
83-
} redisSSL;
84-
8562
/* Forward declaration */
8663
redisContextFuncs redisContextSSLFuncs;
8764

@@ -163,6 +140,22 @@ int redisInitOpenSSL(void)
163140
return REDIS_OK;
164141
}
165142

143+
static int maybeCheckWant(redisSSL *rssl, int rv) {
144+
/**
145+
* If the error is WANT_READ or WANT_WRITE, the appropriate flags are set
146+
* and true is returned. False is returned otherwise
147+
*/
148+
if (rv == SSL_ERROR_WANT_READ) {
149+
rssl->wantRead = 1;
150+
return 1;
151+
} else if (rv == SSL_ERROR_WANT_WRITE) {
152+
rssl->pendingWrite = 1;
153+
return 1;
154+
} else {
155+
return 0;
156+
}
157+
}
158+
166159
/**
167160
* redisSSLContext helper context destruction.
168161
*/
@@ -261,6 +254,42 @@ redisSSLContext *redisCreateSSLContext(const char *cacert_filename, const char *
261254
return NULL;
262255
}
263256

257+
int redisInitiateSSLContinue(redisContext *c) {
258+
if (!c->privctx) {
259+
__redisSetError(c, REDIS_ERR_OTHER, "redisContext is not associated");
260+
return REDIS_ERR;
261+
}
262+
263+
redisSSL *rssl = (redisSSL *)c->privctx;
264+
ERR_clear_error();
265+
int rv = SSL_connect(rssl->ssl);
266+
if (rv == 1) {
267+
c->privctx = rssl;
268+
return REDIS_OK;
269+
}
270+
271+
rv = SSL_get_error(rssl->ssl, rv);
272+
if (((c->flags & REDIS_BLOCK) == 0) &&
273+
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
274+
maybeCheckWant(rssl, rv);
275+
c->privctx = rssl;
276+
return REDIS_OK;
277+
}
278+
279+
if (c->err == 0) {
280+
char err[512];
281+
if (rv == SSL_ERROR_SYSCALL)
282+
snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",strerror(errno));
283+
else {
284+
unsigned long e = ERR_peek_last_error();
285+
snprintf(err,sizeof(err)-1,"SSL_connect failed: %s",
286+
ERR_reason_error_string(e));
287+
}
288+
__redisSetError(c, REDIS_ERR_IO, err);
289+
}
290+
return REDIS_ERR;
291+
}
292+
264293
/**
265294
* SSL Connection initialization.
266295
*/
@@ -295,6 +324,7 @@ static int redisSSLConnect(redisContext *c, SSL *ssl) {
295324
rv = SSL_get_error(rssl->ssl, rv);
296325
if (((c->flags & REDIS_BLOCK) == 0) &&
297326
(rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) {
327+
maybeCheckWant(rssl, rv);
298328
c->privctx = rssl;
299329
return REDIS_OK;
300330
}
@@ -315,6 +345,10 @@ static int redisSSLConnect(redisContext *c, SSL *ssl) {
315345
return REDIS_ERR;
316346
}
317347

348+
redisSSL *redisGetSSLSocket(redisContext *c) {
349+
return c->privctx;
350+
}
351+
318352
/**
319353
* A wrapper around redisSSLConnect() for users who manage their own context and
320354
* create their own SSL object.
@@ -361,22 +395,6 @@ int redisInitiateSSLWithContext(redisContext *c, redisSSLContext *redis_ssl_ctx)
361395
return REDIS_ERR;
362396
}
363397

364-
static int maybeCheckWant(redisSSL *rssl, int rv) {
365-
/**
366-
* If the error is WANT_READ or WANT_WRITE, the appropriate flags are set
367-
* and true is returned. False is returned otherwise
368-
*/
369-
if (rv == SSL_ERROR_WANT_READ) {
370-
rssl->wantRead = 1;
371-
return 1;
372-
} else if (rv == SSL_ERROR_WANT_WRITE) {
373-
rssl->pendingWrite = 1;
374-
return 1;
375-
} else {
376-
return 0;
377-
}
378-
}
379-
380398
/**
381399
* Implementation of redisContextFuncs for SSL connections.
382400
*/

test/redis_client/connection_test.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,6 @@ class SSLConnectionTest < Minitest::Test
218218
include ClientTestHelper
219219
include ConnectionTests
220220

221-
if ENV["DRIVER"] == "hiredis"
222-
def test_tcp_connect_downstream_timeout
223-
skip "TODO: Find the proper way to timeout SSL connections with hiredis"
224-
end
225-
226-
def test_tcp_connect_upstream_timeout
227-
skip "TODO: Find the proper way to timeout SSL connections with hiredis"
228-
end
229-
end
230-
231221
private
232222

233223
def new_client(**overrides)

0 commit comments

Comments
 (0)