diff options
Diffstat (limited to 'sslio.c')
-rw-r--r-- | sslio.c | 343 |
1 files changed, 269 insertions, 74 deletions
@@ -1,5 +1,37 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo <antirez at gmail dot com> + * Copyright (c) 2010-2011, Pieter Noordhuis <pcnoordhuis at gmail dot com> + * Copyright (c) 2019, Redis Labs + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + #include "hiredis.h" -#include "sslio.h" +#include "async.h" #include <assert.h> #ifdef HIREDIS_SSL @@ -7,10 +39,45 @@ #include <errno.h> #include <string.h> +#include <openssl/ssl.h> #include <openssl/err.h> +#include "async_private.h" + void __redisSetError(redisContext *c, int type, const char *str); +/* The SSL context is attached to SSL/TLS connections as a privdata. */ +typedef struct redisSSLContext { + /** + * OpenSSL SSL_CTX; It is optional and will not be set when using + * user-supplied SSL. + */ + SSL_CTX *ssl_ctx; + + /** + * 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; +} redisSSLContext; + +/* Forward declaration */ +redisContextFuncs redisContextSSLFuncs; + #ifdef HIREDIS_SSL_TRACE /** * Callback used for debugging @@ -43,6 +110,16 @@ static void sslLogCallback(const SSL *ssl, int where, int ret) { } #endif +/** + * OpenSSL global initialization and locking handling callbacks. + * Note that this is only required for OpenSSL < 1.1.0. + */ + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define HIREDIS_USE_CRYPTO_LOCKS +#endif + +#ifdef HIREDIS_USE_CRYPTO_LOCKS typedef pthread_mutex_t sslLockType; static void sslLockInit(sslLockType *l) { pthread_mutex_init(l, NULL); @@ -81,102 +158,129 @@ static void initOpensslLocks(void) { } CRYPTO_set_locking_callback(opensslDoLock); } +#endif /* HIREDIS_USE_CRYPTO_LOCKS */ -void redisFreeSsl(redisSsl *ssl){ - if (ssl->ctx) { - SSL_CTX_free(ssl->ctx); +/** + * SSL Connection initialization. + */ + +static int redisSSLConnect(redisContext *c, SSL_CTX *ssl_ctx, SSL *ssl) { + if (c->privdata) { + __redisSetError(c, REDIS_ERR_OTHER, "redisContext was already associated"); + return REDIS_ERR; } - if (ssl->ssl) { - SSL_free(ssl->ssl); + c->privdata = calloc(1, sizeof(redisSSLContext)); + + c->funcs = &redisContextSSLFuncs; + redisSSLContext *rssl = c->privdata; + + rssl->ssl_ctx = ssl_ctx; + rssl->ssl = ssl; + + SSL_set_mode(rssl->ssl, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_set_fd(rssl->ssl, c->fd); + SSL_set_connect_state(rssl->ssl); + + ERR_clear_error(); + int rv = SSL_connect(rssl->ssl); + if (rv == 1) { + 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)) { + 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); } - free(ssl); + return REDIS_ERR; +} + +int redisInitiateSSL(redisContext *c, SSL *ssl) { + return redisSSLConnect(c, NULL, ssl); } -int redisSslCreate(redisContext *c, const char *capath, const char *certpath, - const char *keypath, const char *servername) { - assert(!c->ssl); - c->ssl = calloc(1, sizeof(*c->ssl)); +int redisSecureConnection(redisContext *c, const char *capath, + const char *certpath, const char *keypath, const char *servername) { + + 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; } - redisSsl *s = c->ssl; - s->ctx = SSL_CTX_new(SSLv23_client_method()); #ifdef HIREDIS_SSL_TRACE - SSL_CTX_set_info_callback(s->ctx, sslLogCallback); + SSL_CTX_set_info_callback(ssl_ctx, sslLogCallback); #endif - SSL_CTX_set_mode(s->ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - SSL_CTX_set_options(s->ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); - SSL_CTX_set_verify(s->ctx, SSL_VERIFY_PEER, NULL); - + SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); if ((certpath != NULL && keypath == NULL) || (keypath != NULL && certpath == NULL)) { - __redisSetError(c, REDIS_ERR, "certpath and keypath must be specified together"); - return REDIS_ERR; + __redisSetError(c, REDIS_ERR_OTHER, "certpath and keypath must be specified together"); + goto error; } if (capath) { - if (!SSL_CTX_load_verify_locations(s->ctx, capath, NULL)) { - __redisSetError(c, REDIS_ERR, "Invalid CA certificate"); - return REDIS_ERR; + if (!SSL_CTX_load_verify_locations(ssl_ctx, capath, NULL)) { + __redisSetError(c, REDIS_ERR_OTHER, "Invalid CA certificate"); + goto error; } } if (certpath) { - if (!SSL_CTX_use_certificate_chain_file(s->ctx, certpath)) { - __redisSetError(c, REDIS_ERR, "Invalid client certificate"); - return REDIS_ERR; + 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(s->ctx, keypath, SSL_FILETYPE_PEM)) { - __redisSetError(c, REDIS_ERR, "Invalid client key"); - return REDIS_ERR; + if (!SSL_CTX_use_PrivateKey_file(ssl_ctx, keypath, SSL_FILETYPE_PEM)) { + __redisSetError(c, REDIS_ERR_OTHER, "Invalid client key"); + goto error; } } - s->ssl = SSL_new(s->ctx); - if (!s->ssl) { - __redisSetError(c, REDIS_ERR, "Couldn't create new SSL instance"); - return REDIS_ERR; + ssl = SSL_new(ssl_ctx); + if (!ssl) { + __redisSetError(c, REDIS_ERR_OTHER, "Couldn't create new SSL instance"); + goto error; } if (servername) { - if (!SSL_set_tlsext_host_name(s->ssl, servername)) { - __redisSetError(c, REDIS_ERR, "Couldn't set server name indication"); - return REDIS_ERR; + if (!SSL_set_tlsext_host_name(ssl, servername)) { + __redisSetError(c, REDIS_ERR_OTHER, "Couldn't set server name indication"); + goto error; } } - SSL_set_fd(s->ssl, c->fd); - SSL_set_connect_state(s->ssl); + return redisSSLConnect(c, ssl_ctx, ssl); - c->flags |= REDIS_SSL; - ERR_clear_error(); - int rv = SSL_connect(c->ssl->ssl); - if (rv == 1) { - return REDIS_OK; - } - - rv = SSL_get_error(s->ssl, rv); - if (((c->flags & REDIS_BLOCK) == 0) && - (rv == SSL_ERROR_WANT_READ || rv == SSL_ERROR_WANT_WRITE)) { - 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); - } +error: + if (ssl) SSL_free(ssl); + if (ssl_ctx) SSL_CTX_free(ssl_ctx); return REDIS_ERR; } -static int maybeCheckWant(redisSsl *rssl, int rv) { +static int maybeCheckWant(redisSSLContext *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 @@ -192,15 +296,36 @@ static int maybeCheckWant(redisSsl *rssl, int rv) { } } -int redisSslRead(redisContext *c, char *buf, size_t bufcap) { - int nread = SSL_read(c->ssl->ssl, buf, bufcap); +/** + * Implementation of redisContextFuncs for SSL connections. + */ + +static void redisSSLFreeContext(void *privdata){ + redisSSLContext *rsc = privdata; + + if (!rsc) return; + if (rsc->ssl) { + SSL_free(rsc->ssl); + rsc->ssl = NULL; + } + if (rsc->ssl_ctx) { + SSL_CTX_free(rsc->ssl_ctx); + rsc->ssl_ctx = NULL; + } + free(rsc); +} + +static int redisSSLRead(redisContext *c, char *buf, size_t bufcap) { + redisSSLContext *rssl = c->privdata; + + int nread = SSL_read(rssl->ssl, buf, bufcap); if (nread > 0) { return nread; } else if (nread == 0) { __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); return -1; } else { - int err = SSL_get_error(c->ssl->ssl, nread); + int err = SSL_get_error(rssl->ssl, nread); if (c->flags & REDIS_BLOCK) { /** * In blocking mode, we should never end up in a situation where @@ -223,7 +348,7 @@ int redisSslRead(redisContext *c, char *buf, size_t bufcap) { /** * We can very well get an EWOULDBLOCK/EAGAIN, however */ - if (maybeCheckWant(c->ssl, err)) { + if (maybeCheckWant(rssl, err)) { return 0; } else { __redisSetError(c, REDIS_ERR_IO, NULL); @@ -232,17 +357,19 @@ int redisSslRead(redisContext *c, char *buf, size_t bufcap) { } } -int redisSslWrite(redisContext *c) { - size_t len = c->ssl->lastLen ? c->ssl->lastLen : sdslen(c->obuf); - int rv = SSL_write(c->ssl->ssl, c->obuf, len); +static int redisSSLWrite(redisContext *c) { + redisSSLContext *rssl = c->privdata; + + size_t len = rssl->lastLen ? rssl->lastLen : sdslen(c->obuf); + int rv = SSL_write(rssl->ssl, c->obuf, len); if (rv > 0) { - c->ssl->lastLen = 0; + rssl->lastLen = 0; } else if (rv < 0) { - c->ssl->lastLen = len; + rssl->lastLen = len; - int err = SSL_get_error(c->ssl->ssl, rv); - if ((c->flags & REDIS_BLOCK) == 0 && maybeCheckWant(c->ssl, err)) { + int err = SSL_get_error(rssl->ssl, rv); + if ((c->flags & REDIS_BLOCK) == 0 && maybeCheckWant(rssl, err)) { return 0; } else { __redisSetError(c, REDIS_ERR_IO, NULL); @@ -252,4 +379,72 @@ int redisSslWrite(redisContext *c) { return rv; } +static void redisSSLAsyncRead(redisAsyncContext *ac) { + int rv; + redisSSLContext *rssl = ac->c.privdata; + redisContext *c = &ac->c; + + rssl->wantRead = 0; + + if (rssl->pendingWrite) { + int done; + + /* This is probably just a write event */ + rssl->pendingWrite = 0; + rv = redisBufferWrite(c, &done); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + return; + } else if (!done) { + _EL_ADD_WRITE(ac); + } + } + + rv = redisBufferRead(c); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + _EL_ADD_READ(ac); + redisProcessCallbacks(ac); + } +} + +static void redisSSLAsyncWrite(redisAsyncContext *ac) { + int rv, done = 0; + redisSSLContext *rssl = ac->c.privdata; + redisContext *c = &ac->c; + + rssl->pendingWrite = 0; + rv = redisBufferWrite(c, &done); + if (rv == REDIS_ERR) { + __redisAsyncDisconnect(ac); + return; + } + + if (!done) { + if (rssl->wantRead) { + /* Need to read-before-write */ + rssl->pendingWrite = 1; + _EL_DEL_WRITE(ac); + } else { + /* No extra reads needed, just need to write more */ + _EL_ADD_WRITE(ac); + } + } else { + /* Already done! */ + _EL_DEL_WRITE(ac); + } + + /* Always reschedule a read */ + _EL_ADD_READ(ac); +} + +redisContextFuncs redisContextSSLFuncs = { + .free_privdata = redisSSLFreeContext, + .async_read = redisSSLAsyncRead, + .async_write = redisSSLAsyncWrite, + .read = redisSSLRead, + .write = redisSSLWrite +}; + #endif |