diff --git a/ext/hiredis_ext/connection.c b/ext/hiredis_ext/connection.c index 865c842..9da6e61 100644 --- a/ext/hiredis_ext/connection.c +++ b/ext/hiredis_ext/connection.c @@ -1,6 +1,8 @@ #include #include +#include #include "hiredis_ext.h" +#include "hiredis_ssl.h" typedef struct redisParentContext { redisContext *context; @@ -236,6 +238,10 @@ static VALUE connection_generic_connect(VALUE self, redisContext *c, VALUE arg_t rb_sys_fail(0); } +static char *nullable_cstr_arg(VALUE arg) { + return NIL_P(arg) ? NULL : StringValueCStr(arg); +} + static VALUE connection_connect(int argc, VALUE *argv, VALUE self) { redisContext *c; VALUE arg_host = Qnil; @@ -262,6 +268,81 @@ static VALUE connection_connect(int argc, VALUE *argv, VALUE self) { return connection_generic_connect(self,c,arg_timeout); } +static VALUE connection_connect_ssl(int argc, VALUE *argv, VALUE self) { + redisContext *c; + VALUE arg_host = Qnil; + VALUE arg_port = Qnil; + VALUE arg_timeout = Qnil; + VALUE ca_file = Qnil; + VALUE ca_path = Qnil; + VALUE client_ca = Qnil; + VALUE client_key = Qnil; + + redisSSLContext *ssl_context; + redisSSLContextError ssl_error = REDIS_SSL_CTX_NONE; + + if (argc >= 2 && argc <= 4) { + arg_host = argv[0]; + arg_port = argv[1]; + + if (argc == 3) { + arg_timeout = argv[2]; + + /* Sanity check */ + if (NUM2INT(arg_timeout) <= 0) { + rb_raise(rb_eArgError, "timeout should be positive"); + } + } + + if (argc == 4) { + // See OpenSSL::SSL::SSLContext for available options. Supported options: + // ca_file, ca_path, client_ca, key + VALUE ssl_options = argv[3]; + + if (ssl_options != Qnil) { + ca_file = rb_hash_aref(ssl_options, ID2SYM(rb_intern("ca_file"))); + ca_path = rb_hash_aref(ssl_options, ID2SYM(rb_intern("ca_path"))); + client_ca = rb_hash_aref(ssl_options, ID2SYM(rb_intern("client_ca"))); + client_key = rb_hash_aref(ssl_options, ID2SYM(rb_intern("client_key"))); + } + } + } else { + rb_raise(rb_eArgError, "invalid number of arguments"); + } + + redisInitOpenSSL(); + ssl_context = redisCreateSSLContext( + nullable_cstr_arg(ca_file), + nullable_cstr_arg(ca_path), + nullable_cstr_arg(client_ca), + nullable_cstr_arg(client_key), + nullable_cstr_arg(arg_host), + &ssl_error); + + if (ssl_context == NULL || ssl_error != REDIS_SSL_CTX_NONE) { + rb_raise(rb_eRuntimeError, "error creating SSL context: %s", + (ssl_error != 0) ? redisSSLContextGetError(ssl_error) : "unknown error"); + } + + c = redisConnectNonBlock(StringValuePtr(arg_host), NUM2INT(arg_port)); + + if (c == NULL || c->err) { + if (c) { + redisFree(c); + rb_raise(rb_eRuntimeError, "Basic connection to Redis failed! Error: %s\n", c->errstr); + } else { + rb_raise(rb_eRuntimeError, "Basic connection to Redis failed! Error: can't allocate redis context"); + } + } + + if (redisInitiateSSLWithContext(c, ssl_context) != REDIS_OK) { + redisFree(c); + rb_raise(rb_eRuntimeError, "SSL connection to Redis failed! Error: %s\n", c->errstr); + } + + return connection_generic_connect(self,c,arg_timeout); +} + static VALUE connection_connect_unix(int argc, VALUE *argv, VALUE self) { redisContext *c; VALUE arg_path = Qnil; @@ -503,6 +584,7 @@ void InitConnection(VALUE mod) { rb_global_variable(&klass_connection); rb_define_alloc_func(klass_connection, connection_parent_context_alloc); rb_define_method(klass_connection, "connect", connection_connect, -1); + rb_define_method(klass_connection, "connect_ssl", connection_connect_ssl, -1); rb_define_method(klass_connection, "connect_unix", connection_connect_unix, -1); rb_define_method(klass_connection, "connected?", connection_is_connected, 0); rb_define_method(klass_connection, "disconnect", connection_disconnect, 0); diff --git a/ext/hiredis_ext/extconf.rb b/ext/hiredis_ext/extconf.rb index da39eb5..cfd97b4 100644 --- a/ext/hiredis_ext/extconf.rb +++ b/ext/hiredis_ext/extconf.rb @@ -1,12 +1,45 @@ require 'mkmf' +openssl_include_dir, openssl_lib_dir = dir_config('openssl') + build_hiredis = true + +with_ssl = with_config('ssl', true) + unless have_header('sys/socket.h') puts "Could not find (Likely Windows)." puts "Skipping building hiredis. The slower, pure-ruby implementation will be used instead." build_hiredis = false end +def find_openssl_library + return false unless find_header("openssl/ssl.h") + + ret = find_library("crypto", "CRYPTO_malloc") && + find_library("ssl", "SSL_new") + + return ret if ret + + false +end + +if with_ssl + Logging.message "=== Checking for SSL... ===\n" + pkg_config_found = pkg_config("openssl") && find_header("openssl/ssl.h") + + if !pkg_config_found && !find_openssl_library + use_ssl = false + + Logging.message "=== SSL not found, skipping rediss:// support. ===\n" + Logging.message "Makefile wasn't created. Fix the errors above.\n" + + raise "OpenSSL library could not be found.\n" + "You can disable SSL support using the --without-ssl option. " \ + "You might want to use --with-openssl-dir= option to specify the prefix where OpenSSL " \ + "is installed." + end +end + RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC'] hiredis_dir = File.join(File.dirname(__FILE__), %w{.. .. vendor hiredis}) @@ -27,15 +60,22 @@ end if build_hiredis + build_cflags = "-I#{openssl_include_dir}" if openssl_include_dir + build_ldflags = "-L#{openssl_lib_dir}" if openssl_lib_dir + # Set the prefix to ensure we don't mix and match headers or libraries + prefix = File.dirname(openssl_include_dir) if openssl_include_dir + ssl_make_arg = "USE_SSL=1 CFLAGS=#{build_cflags} SSL_LDFLAGS=#{build_ldflags} OPENSSL_PREFIX=#{prefix}" if with_ssl + # Make sure hiredis is built... Dir.chdir(hiredis_dir) do - success = system("#{make_program} static") + success = system("#{ssl_make_arg} #{make_program} static") raise "Building hiredis failed" if !success end # Statically link to hiredis (mkmf can't do this for us) $CFLAGS << " -I#{hiredis_dir}" $LDFLAGS << " #{hiredis_dir}/libhiredis.a" + $LDFLAGS << " #{hiredis_dir}/libhiredis_ssl.a" if with_ssl have_func("rb_thread_fd_select") create_makefile('hiredis/ext/hiredis_ext')