Skip to content

Commit df3004a

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 df3004a

File tree

6 files changed

+131
-59
lines changed

6 files changed

+131
-59
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: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,22 @@
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
50+
51+
$CFLAGS << " -Wno-unused-parameter" # VALUE self has to be there but we don't care what it is.
52+
$CFLAGS << " -Wno-keyword-macro" # hiding return
53+
$CFLAGS << " -Wno-gcc-compat" # ruby.h 2.6.0 on macos 10.14, dunno
54+
$CFLAGS << " -Wno-compound-token-split-by-macro"
4355

4456
create_makefile("redis_client/hiredis_connection")
4557
else

ext/redis_client/hiredis/hiredis_connection.c

Lines changed: 27 additions & 6 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

@@ -44,7 +45,6 @@ typedef struct {
4445
redisSSLContext *context;
4546
} hiredis_ssl_context_t;
4647

47-
4848
#define ENSURE_CONNECTED(connection) if (!connection->context) rb_raise(rb_eRuntimeError, "[BUG] not connected");
4949

5050
#define SSL_CONTEXT(from, name) \
@@ -161,7 +161,7 @@ static void *reply_create_array(const redisReadTask *task, size_t elements) {
161161
value = rb_hash_new();
162162
break;
163163
case REDIS_REPLY_SET:
164-
value = rb_funcall(rb_cSet, id_new, 0);
164+
value = rb_funcallv(rb_cSet, id_new, 0, NULL);
165165
break;
166166
default:
167167
rb_bug("[hiredis] Unexpected create array type %d", task->parent->type);
@@ -183,8 +183,6 @@ static void *reply_create_bool(const redisReadTask *task, int bval) {
183183
reply_append(task, bval ? Qtrue : Qfalse);
184184
// Qfalse == NULL, so we can't return Qfalse as it would be interpreted as out of memory error.
185185
// So we return Qnil instead.
186-
assert(Qfalse == NULL);
187-
assert(Qnil != NULL);
188186
return (void*)(bval ? Qtrue : Qnil);
189187
}
190188

@@ -380,10 +378,9 @@ static int hiredis_wait_writable(int fd, const struct timeval *timeout, int *iss
380378
struct timeval to;
381379
struct timeval *toptr = NULL;
382380

383-
rb_fdset_t fds;
384-
385381
/* Be cautious: a call to rb_fd_init to initialize the rb_fdset_t structure
386382
* must be paired with a call to rb_fd_term to free it. */
383+
rb_fdset_t fds;
387384
rb_fd_init(&fds);
388385
rb_fd_set(fd, &fds);
389386

@@ -466,6 +463,24 @@ static VALUE hiredis_init_ssl(VALUE self, VALUE ssl_param) {
466463
if (redisInitiateSSLWithContext(connection->context, ssl_context->context) != REDIS_OK) {
467464
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
468465
}
466+
467+
redisSSL *redis_ssl = redisGetSSLSocket(connection->context);
468+
469+
if (redis_ssl->wantRead) {
470+
int readable = 0;
471+
if (hiredis_wait_readable(connection->context->fd, &connection->connect_timeout, &readable) < 0) {
472+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
473+
}
474+
if (!readable) {
475+
errno = EAGAIN;
476+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
477+
}
478+
479+
if (redisInitiateSSLContinue(connection->context) != REDIS_OK) {
480+
hiredis_raise_error_and_disconnect(connection, rb_eRedisClientConnectTimeoutError);
481+
};
482+
}
483+
469484
return Qtrue;
470485
}
471486

@@ -622,6 +637,12 @@ static VALUE hiredis_close(VALUE self) {
622637
}
623638

624639
void Init_hiredis_connection(void) {
640+
#ifdef RUBY_ASSERT
641+
// Qfalse == NULL, so we can't return Qfalse in `reply_create_bool()`
642+
RUBY_ASSERT((void *)Qfalse == NULL);
643+
RUBY_ASSERT((void *)Qnil != NULL);
644+
#endif
645+
625646
redisInitOpenSSL();
626647

627648
id_parse = rb_intern("parse");

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)