diff --git a/Makefile b/Makefile index 07b8a83..ea96419 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ # Copyright (C) 2010-2011 Pieter Noordhuis # This file is released under the BSD license, see the COPYING file -OBJ=net.o hiredis.o sds.o async.o read.o -EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev hiredis-example-glib +OBJ=net.o hiredis.o sds.o async.o read.o sslio.o +EXAMPLES=hiredis-example hiredis-example-libevent hiredis-example-libev hiredis-example-glib hiredis-example-ssl TESTS=hiredis-test LIBNAME=libhiredis PKGCONFNAME=hiredis.pc @@ -53,6 +53,10 @@ DYLIB_MAKE_CMD=$(CC) -shared -Wl,-soname,$(DYLIB_MINOR_NAME) -o $(DYLIBNAME) $(L STLIBNAME=$(LIBNAME).$(STLIBSUFFIX) STLIB_MAKE_CMD=$(AR) rcs $(STLIBNAME) +OPENSSL_PREFIX=/usr/local/opt/openssl +CFLAGS+=-I$(OPENSSL_PREFIX)/include +LDFLAGS+=-L$(OPENSSL_PREFIX)/lib -lssl -lcrypto + # Platform-specific overrides uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') ifeq ($(uname_S),SunOS) @@ -70,10 +74,11 @@ all: $(DYLIBNAME) $(STLIBNAME) hiredis-test $(PKGCONFNAME) # Deps (use make dep to generate this) async.o: async.c fmacros.h async.h hiredis.h read.h sds.h net.h dict.c dict.h dict.o: dict.c fmacros.h dict.h -hiredis.o: hiredis.c fmacros.h hiredis.h read.h sds.h net.h +hiredis.o: hiredis.c fmacros.h hiredis.h read.h sds.h net.h sslio.h net.o: net.c fmacros.h net.h hiredis.h read.h sds.h read.o: read.c fmacros.h read.h sds.h sds.o: sds.c sds.h +sslio.o: sslio.c sslio.h hiredis.h test.o: test.c fmacros.h hiredis.h read.h sds.h $(DYLIBNAME): $(OBJ) @@ -101,6 +106,9 @@ hiredis-example-ivykis: examples/example-ivykis.c adapters/ivykis.h $(STLIBNAME) hiredis-example-macosx: examples/example-macosx.c adapters/macosx.h $(STLIBNAME) $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< -framework CoreFoundation $(STLIBNAME) +hiredis-example-ssl: examples/example-ssl.c $(STLIBNAME) + $(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. $< $(STLIBNAME) + ifndef AE_DIR hiredis-example-ae: @echo "Please specify AE_DIR (e.g. /src)" @@ -158,7 +166,7 @@ clean: rm -rf $(DYLIBNAME) $(STLIBNAME) $(TESTS) $(PKGCONFNAME) examples/hiredis-example* *.o *.gcda *.gcno *.gcov dep: - $(CC) -MM *.c + $(CC) $(CPPFLAGS) $(CFLAGS) -MM *.c INSTALL?= cp -pPR diff --git a/hiredis.c b/hiredis.c index bfbf483..fae8094 100644 --- a/hiredis.c +++ b/hiredis.c @@ -42,6 +42,7 @@ #include "hiredis.h" #include "net.h" #include "sds.h" +#include "sslio.h" static redisReply *createReplyObject(int type); static void *createStringObject(const redisReadTask *task, char *str, size_t len); @@ -614,7 +615,9 @@ void redisFree(redisContext *c) { free(c->unix_sock.path); free(c->timeout); free(c->saddr); - free(c); + if (c->ssl) { + redisFreeSsl(c->ssl); + } } int redisFreeKeepFd(redisContext *c) { @@ -760,6 +763,11 @@ redisContext *redisConnectFd(int fd) { return c; } +int redisSecureConnection(redisContext *c, const char *caPath, + const char *certPath, const char *keyPath) { + return redisSslCreate(c, caPath, certPath, keyPath); +} + /* Set read/write timeout on a blocking socket. */ int redisSetTimeout(redisContext *c, const struct timeval tv) { if (c->flags & REDIS_BLOCK) @@ -774,6 +782,24 @@ int redisEnableKeepAlive(redisContext *c) { return REDIS_OK; } +static int rawRead(redisContext *c, char *buf, size_t bufcap) { + int nread = read(c->fd, buf, bufcap); + if (nread == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + return 0; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } else if (nread == 0) { + __redisSetError(c, REDIS_ERR_EOF, "Server closed the connection"); + return -1; + } else { + return nread; + } +} + /* Use this function to handle a read event on the descriptor. It will try * and read some bytes from the socket and feed them to the reply parser. * @@ -787,24 +813,31 @@ int redisBufferRead(redisContext *c) { if (c->err) return REDIS_ERR; - nread = read(c->fd,buf,sizeof(buf)); - if (nread == -1) { + nread = c->flags & REDIS_SSL ? + redisSslRead(c, buf, sizeof(buf)) : rawRead(c, buf, sizeof(buf)); + if (nread > 0) { + if (redisReaderFeed(c->reader, buf, nread) != REDIS_OK) { + __redisSetError(c, c->reader->err, c->reader->errstr); + return REDIS_ERR; + } else { + } + } else if (nread < 0) { + return REDIS_ERR; + } + return REDIS_OK; +} + +static int rawWrite(redisContext *c) { + int nwritten = write(c->fd, c->obuf, sdslen(c->obuf)); + if (nwritten < 0) { if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { /* Try again later */ } else { - __redisSetError(c,REDIS_ERR_IO,NULL); - return REDIS_ERR; - } - } else if (nread == 0) { - __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection"); - return REDIS_ERR; - } else { - if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) { - __redisSetError(c,c->reader->err,c->reader->errstr); - return REDIS_ERR; + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; } } - return REDIS_OK; + return nwritten; } /* Write the output buffer to the socket. @@ -817,21 +850,15 @@ int redisBufferRead(redisContext *c) { * c->errstr to hold the appropriate error string. */ int redisBufferWrite(redisContext *c, int *done) { - int nwritten; /* Return early when the context has seen an error. */ if (c->err) return REDIS_ERR; if (sdslen(c->obuf) > 0) { - nwritten = write(c->fd,c->obuf,sdslen(c->obuf)); - if (nwritten == -1) { - if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { - /* Try again later */ - } else { - __redisSetError(c,REDIS_ERR_IO,NULL); - return REDIS_ERR; - } + int nwritten = (c->flags & REDIS_SSL) ? redisSslWrite(c) : rawWrite(c); + if (nwritten < 0) { + return REDIS_ERR; } else if (nwritten > 0) { if (nwritten == (signed)sdslen(c->obuf)) { sdsfree(c->obuf); diff --git a/hiredis.h b/hiredis.h index 1b0d5e6..29c0253 100644 --- a/hiredis.h +++ b/hiredis.h @@ -74,6 +74,9 @@ /* Flag that is set when we should set SO_REUSEADDR before calling bind() */ #define REDIS_REUSEADDR 0x80 +/* Flag that is set when this connection is done through SSL */ +#define REDIS_SSL 0x100 + #define REDIS_KEEPALIVE_INTERVAL 15 /* seconds */ /* number of times we retry to connect in the case of EADDRNOTAVAIL and @@ -112,6 +115,8 @@ enum redisConnectionType { REDIS_CONN_UNIX }; +struct redisSsl; + /* Context for a connection to Redis */ typedef struct redisContext { int err; /* Error flags, 0 when there is no error */ @@ -137,6 +142,9 @@ typedef struct redisContext { /* For non-blocking connect */ struct sockadr *saddr; size_t addrlen; + /* For SSL communication */ + struct redisSsl *ssl; + } redisContext; redisContext *redisConnect(const char *ip, int port); @@ -151,6 +159,13 @@ redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval redisContext *redisConnectUnixNonBlock(const char *path); redisContext *redisConnectFd(int fd); +/** + * Secure the connection using SSL. This should be done before any command is + * executed on the connection. + */ +int redisSecureConnection(redisContext *c, const char *capath, const char *certpath, + const char *keypath); + /** * Reconnect the given context using the saved information. * diff --git a/sslio.c b/sslio.c new file mode 100644 index 0000000..9ee015b --- /dev/null +++ b/sslio.c @@ -0,0 +1,197 @@ +#include "hiredis.h" +#include "sslio.h" + +#include +#ifndef HIREDIS_NOSSL +#include + +void __redisSetError(redisContext *c, int type, const char *str); + +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)); + } +} + +typedef pthread_mutex_t sslLockType; +static void sslLockInit(sslLockType *l) { + pthread_mutex_init(l, NULL); +} +static void sslLockAcquire(sslLockType *l) { + pthread_mutex_lock(l); +} +static void sslLockRelease(sslLockType *l) { + pthread_mutex_unlock(l); +} +static pthread_mutex_t *ossl_locks; + +static void opensslDoLock(int mode, int lkid, const char *f, int line) { + sslLockType *l = ossl_locks + lkid; + + if (mode & CRYPTO_LOCK) { + sslLockAcquire(l); + } else { + sslLockRelease(l); + } + + (void)f; + (void)line; +} + +static void initOpensslLocks(void) { + unsigned ii, nlocks; + if (CRYPTO_get_locking_callback() != NULL) { + /* Someone already set the callback before us. Don't destroy it! */ + return; + } + nlocks = CRYPTO_num_locks(); + ossl_locks = malloc(sizeof(*ossl_locks) * nlocks); + for (ii = 0; ii < nlocks; ii++) { + sslLockInit(ossl_locks + ii); + } + CRYPTO_set_locking_callback(opensslDoLock); +} + +void redisFreeSsl(redisSsl *ssl){ + if (ssl->ctx) { + SSL_CTX_free(ssl->ctx); + } + if (ssl->ssl) { + SSL_free(ssl->ssl); + } + free(ssl); +} + +int redisSslCreate(redisContext *c, const char *capath, const char *certpath, + const char *keypath) { + assert(!c->ssl); + c->ssl = calloc(1, sizeof(*c->ssl)); + static int isInit = 0; + if (!isInit) { + isInit = 1; + SSL_library_init(); + initOpensslLocks(); + } + + redisSsl *s = c->ssl; + s->ctx = SSL_CTX_new(SSLv23_client_method()); + SSL_CTX_set_info_callback(s->ctx, sslLogCallback); + 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); + + if ((certpath != NULL && keypath == NULL) || (keypath != NULL && certpath == NULL)) { + __redisSetError(c, REDIS_ERR, "certpath and keypath must be specified together"); + return REDIS_ERR; + } + + if (capath) { + if (!SSL_CTX_load_verify_locations(s->ctx, capath, NULL)) { + __redisSetError(c, REDIS_ERR, "Invalid CA certificate"); + return REDIS_ERR; + } + } + 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_PrivateKey_file(s->ctx, keypath, SSL_FILETYPE_PEM)) { + __redisSetError(c, REDIS_ERR, "Invalid client key"); + return REDIS_ERR; + } + printf("Loaded certificate!\n"); + } + + s->ssl = SSL_new(s->ctx); + if (!s->ssl) { + __redisSetError(c, REDIS_ERR, "Couldn't create new SSL instance"); + return REDIS_ERR; + } + + SSL_set_fd(s->ssl, c->fd); + SSL_set_connect_state(s->ssl); + + c->flags |= REDIS_SSL; + printf("Before SSL_connect()\n"); + int rv = SSL_connect(c->ssl->ssl); + if (rv == 1) { + printf("SSL_connect() success!\n"); + return REDIS_OK; + } + printf("ConnectRV: %d\n", rv); + + 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) { + __redisSetError(c, REDIS_ERR_IO, "SSL_connect() failed"); + printf("rv: %d\n", rv); + } + printf("ERROR!!!\n"); + return REDIS_ERR; +} + +int redisSslRead(redisContext *c, char *buf, size_t bufcap) { + int nread = SSL_read(c->ssl->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); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 0; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } +} + +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); + + if (rv > 0) { + c->ssl->lastLen = 0; + } else if (rv < 0) { + c->ssl->lastLen = len; + + int err = SSL_get_error(c->ssl->ssl, rv); + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return 0; + } else { + __redisSetError(c, REDIS_ERR_IO, NULL); + return -1; + } + } + return rv; +} + +#endif diff --git a/sslio.h b/sslio.h new file mode 100644 index 0000000..a410cb3 --- /dev/null +++ b/sslio.h @@ -0,0 +1,48 @@ +#ifndef REDIS_SSLIO_H +#define REDIS_SSLIO_H + + +#ifdef HIREDIS_NOSSL +typedef struct redisSsl { + int dummy; +} redisSsl; +static void redisFreeSsl(redisSsl *) { +} +static int redisSslCreate(struct redisContext *c) { + return REDIS_ERR; +} +static int redisSslRead(struct redisContect *c, char *s, size_t, n) { + return -1; +} +static int redisSslWrite(struct redisContext *c) { + return -1; +} +#else +#include + +/** + * This file contains routines for HIREDIS' SSL + */ + +typedef struct redisSsl { + SSL *ssl; + SSL_CTX *ctx; + + /** + * 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; +} redisSsl; + +struct redisContext; + +void redisFreeSsl(redisSsl *); +int redisSslCreate(struct redisContext *c, const char *caPath, + const char *certPath, const char *keyPath); + +int redisSslRead(struct redisContext *c, char *buf, size_t bufcap); +int redisSslWrite(struct redisContext *c); + +#endif /* !HIREDIS_NOSSL */ +#endif /* HIREDIS_SSLIO_H */