--- parser3/src/classes/redis.C 2026/10/06 23:57:02 1.3 +++ parser3/src/classes/redis.C 2026/10/09 21:10:38 1.7 @@ -14,15 +14,17 @@ #include "pa_vbool.h" #include "pa_vfile.h" -volatile const char * IDENT_REDIS_C="$Id: redis.C,v 1.3 2026/10/06 23:57:02 moko Exp $" IDENT_PA_VREDIS_H; +volatile const char * IDENT_REDIS_C="$Id: redis.C,v 1.7 2026/10/09 21:10:38 moko Exp $" IDENT_PA_VREDIS_H; // defines #define DEFAULT_TIMEOUT 2 // seconds +// ^r.f-get[key], ^r.f-call[GET;key]: the strings of the reply are files +#define FILE_PREFIX "f-" class MRedis: public Methoded { public: // VStateless_class - Value* create_new_value(Pool&) { return new VRedis(); } + Value* create_new_value(Pool& apool) { return new VRedis(apool); } public: MRedis(); }; @@ -44,6 +46,13 @@ static void _open(Request& r, MethodPara int db=0; int protocol=3; int auto_reconnect=0; +#ifdef WITH_REDIS_SSL + bool tls_specified=false; + const char* tls_ca=0; + const char* tls_cert=0; + const char* tls_key=0; + bool tls_verify=true; +#endif if(params.count()>0) if(HashStringValue* options=params.as_hash(0)) { @@ -72,6 +81,27 @@ static void _open(Request& r, MethodPara throw Exception(PARSER_RUNTIME, 0, "protocol must be 2 or 3"); } else if(key=="auto_reconnect") { auto_reconnect=r.process(*value).as_int(); + } else if(key=="tls") { +#ifdef WITH_REDIS_SSL + tls_specified=true; + if(HashStringValue* tls_options=value->get_hash()) + for(HashStringValue::Iterator t(*tls_options); t; t.next()) { + String::Body tkey=t.key(); + Value* tvalue=t.value(); + if(tkey=="ca") { + tls_ca=tvalue->as_string().cstr(); + } else if(tkey=="cert") { + tls_cert=tvalue->as_string().cstr(); + } else if(tkey=="key") { + tls_key=tvalue->as_string().cstr(); + } else if(tkey=="verify") { + tls_verify=r.process(*tvalue).as_bool(); + } else + throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); + } +#else + throw Exception("redis", 0, "compiled without redis SSL support"); +#endif } else throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); } @@ -96,21 +126,8 @@ static void _open(Request& r, MethodPara } // ^r.open[] of an opened object: close the old connection first - if(self.fcontext) { - redisFree(self.fcontext); - self.fcontext=0; - } - - redisContext* context=redisConnectWithOptions(&options); - if(!context) - throw Exception("redis", 0, "connect failed: out of memory"); - if(context->err) { - const char* error=pa_strdup(context->errstr); - redisFree(context); - throw Exception("redis", 0, "connect failed: %s", error); - } + self.release(); - self.fcontext=context; self.fuser=user; self.fpassword=password; self.fdb=db; @@ -118,31 +135,53 @@ static void _open(Request& r, MethodPara self.fauto_reconnect=auto_reconnect; try { +#ifdef WITH_REDIS_SSL + if(tls_specified) { + static bool ssl_initialized=false; // OpenSSL before 1.1 needs it once + if(!ssl_initialized) { + redisInitOpenSSL(); + ssl_initialized=true; + } + + redisSSLOptions ssl_options; + memset(&ssl_options, 0, sizeof(ssl_options)); + ssl_options.cacert_filename=tls_ca; + ssl_options.cert_filename=tls_cert; + ssl_options.private_key_filename=tls_key; + ssl_options.server_name=host; + // as amqp: the server certificate is checked only against the given ca + ssl_options.verify_mode=tls_verify && tls_ca ? REDIS_SSL_VERIFY_PEER : REDIS_SSL_VERIFY_NONE; + + redisSSLContextError error=REDIS_SSL_CTX_NONE; + if(!(self.fssl_context=redisCreateSSLContextWithOptions(&ssl_options, &error))) + throw Exception("redis", 0, "SSL context failed: %s", redisSSLContextGetError(error)); + } +#endif + + if(!(self.fcontext=redisConnectWithOptions(&options))) + throw Exception("redis", 0, "connect failed: out of memory"); + if(self.fcontext->err) + throw Exception("redis", 0, "connect failed: %s", self.fcontext->errstr); + self.handshake(); } catch(...) { - redisFree(context); - self.fcontext=0; + self.release(); throw; } } static void _release(Request& r, MethodParams&) { - VRedis& self=GET_SELF(r, VRedis); - - if(self.fcontext) { - redisFree(self.fcontext); - self.fcontext=0; - } + GET_SELF(r, VRedis).release(); } -static Value* reply_value(redisReply& reply, bool binary, const String& command) { +static Value* reply_value(redisReply& reply, bool as_file, const String& command) { switch(reply.type) { case REDIS_REPLY_STRING: case REDIS_REPLY_STATUS: case REDIS_REPLY_VERB: case REDIS_REPLY_BIGNUM: { char* data=pa_strdup(reply.str, reply.len); - if(binary) { + if(as_file) { VFile& file=*new VFile; file.set_binary(true/*tainted*/, data, reply.len); return &file; @@ -166,13 +205,13 @@ static Value* reply_value(redisReply& re VArray& result=*new VArray(reply.elements); ArrayValue& array=result.array(); for(size_t i=0; ias_string(), reply_value(*reply.element[i+1], binary, command)); + result.hash().put(reply_value(*reply.element[i], false, command)->as_string(), reply_value(*reply.element[i+1], as_file, command)); return &result; } case REDIS_REPLY_ERROR: @@ -202,7 +241,7 @@ static void add_argument(Array argv; @@ -231,22 +270,27 @@ static void redis_command(Request& r, Me redisReply* reply=self.command((int)argv.count(), argv.ptr(0), lengths.ptr(0)); Temp_redis_reply temp_reply(reply); - r.write(*reply_value(*reply, binary, command)); + r.write(*reply_value(*reply, as_file, command)); } -// ^r.call[command;arguments...] static void _call(Request& r, MethodParams& params) { redis_command(r, params, params.as_string(0, "command must be string"), 1, false); } -// ^r.call_binary[command;arguments...]: the strings of the reply are files -static void _call_binary(Request& r, MethodParams& params) { +static void _file_call(Request& r, MethodParams& params) { redis_command(r, params, params.as_string(0, "command must be string"), 1, true); } // ^r.command_name[arguments...]: the command is the name of the method called, see VRedis::get_element4call void redis_command_method(Request& r, MethodParams& params) { - redis_command(r, params, *r.get_method_frame()->method.name, 0, false); + const String& name=*r.get_method_frame()->method.name; + if(name.starts_with(FILE_PREFIX)) { + size_t prefix_length=strlen(FILE_PREFIX); + if(name.length()==prefix_length) + throw Exception(PARSER_RUNTIME, &name, "command name expected after " FILE_PREFIX); + redis_command(r, params, name.mid(prefix_length, name.length()), 0, true); + } else + redis_command(r, params, name, 0, false); } #else @@ -258,15 +302,16 @@ static void _open(Request&, MethodParams #endif // WITH_REDIS MRedis::MRedis(): Methoded("redis") { - // ^redis::open[ $.host[localhost] $.port(6379) $.unix_socket[path] $.user[] $.password[] $.db(0) $.timeout(seconds, 0 is none) $.protocol(3) $.auto_reconnect(seconds) ] + // ^redis::open[ $.host[localhost] $.port(6379) $.unix_socket[path] $.user[] $.password[] $.db(0) $.timeout(seconds, 0 is none) $.protocol(3) $.auto_reconnect(seconds) + // $.tls[ $.ca[path] $.cert[path] $.key[path] $.verify(true) ] ] add_native_method("open", Method::CT_DYNAMIC, _open, 0, 1); #ifdef WITH_REDIS // ^r.call[command;arguments...] add_native_method("call", Method::CT_DYNAMIC, _call, 1, MAX_REDIS_COMMAND_PARAMS); - // ^r.call_binary[command;arguments...] - add_native_method("call_binary", Method::CT_DYNAMIC, _call_binary, 1, MAX_REDIS_COMMAND_PARAMS); + // ^r.f-call[command;arguments...] + add_native_method(FILE_PREFIX "call", Method::CT_DYNAMIC, _file_call, 1, MAX_REDIS_COMMAND_PARAMS); // ^r.release[] add_native_method("release", Method::CT_DYNAMIC, _release, 0, 0);