Annotation of parser3/src/classes/redis.C, revision 1.6

1.1       moko        1: /** @file
                      2:        Parser: @b redis parser class.
                      3: 
                      4:        Copyright (c) 2001-2026 Art. Lebedev Studio (https://www.artlebedev.com)
                      5:        Authors: Konstantin Morshnev <moko@design.ru>
                      6: */
                      7: 
1.3       moko        8: #include "classes.h"
                      9: #include "pa_vmethod_frame.h"
                     10: 
1.2       moko       11: #include "pa_vredis.h"
                     12: #include "pa_request.h"
1.1       moko       13: #include "pa_varray.h"
                     14: #include "pa_vbool.h"
                     15: #include "pa_vfile.h"
                     16: 
1.6     ! moko       17: volatile const char * IDENT_REDIS_C="$Id: redis.C,v 1.5 2026/10/07 15:56:17 moko Exp $" IDENT_PA_VREDIS_H;
1.1       moko       18: 
                     19: // defines
                     20: 
                     21: #define DEFAULT_TIMEOUT 2 // seconds
1.5       moko       22: // ^r.f-get[key], ^r.f-call[GET;key]: the strings of the reply are files
                     23: #define FILE_PREFIX "f-"
1.1       moko       24: 
                     25: class MRedis: public Methoded {
                     26: public: // VStateless_class
                     27:        Value* create_new_value(Pool&) { return new VRedis(); }
                     28: public:
                     29:        MRedis();
                     30: };
                     31: 
                     32: DECLARE_CLASS_VAR(redis, new MRedis);
                     33: 
                     34: #ifdef WITH_REDIS
                     35: 
                     36: static void _open(Request& r, MethodParams& params) {
                     37:        VRedis& self=GET_SELF(r, VRedis);
                     38: 
                     39:        const char* host="localhost";
                     40:        int port=6379;
                     41:        bool tcp_specified=false;
                     42:        const char* unix_socket=0;
                     43:        double timeout=DEFAULT_TIMEOUT;
                     44:        const char* user=0;
                     45:        const char* password=0;
                     46:        int db=0;
                     47:        int protocol=3;
                     48:        int auto_reconnect=0;
1.6     ! moko       49: #ifdef WITH_REDIS_SSL
        !            50:        bool tls_specified=false;
        !            51:        const char* tls_ca=0;
        !            52:        const char* tls_cert=0;
        !            53:        const char* tls_key=0;
        !            54:        bool tls_verify=true;
        !            55: #endif
1.1       moko       56: 
                     57:        if(params.count()>0)
                     58:                if(HashStringValue* options=params.as_hash(0)) {
                     59:                        for(HashStringValue::Iterator i(*options); i; i.next()) {
                     60:                                String::Body key=i.key();
                     61:                                Value* value=i.value();
                     62:                                if(key=="host") {
                     63:                                        host=value->as_string().cstr();
                     64:                                        tcp_specified=true;
                     65:                                } else if(key=="port") {
                     66:                                        port=r.process(*value).as_int();
                     67:                                        tcp_specified=true;
                     68:                                } else if(key=="unix_socket") {
                     69:                                        unix_socket=value->as_string().cstr();
                     70:                                } else if(key=="timeout") {
                     71:                                        timeout=r.process(*value).as_double();
                     72:                                } else if(key=="user") {
                     73:                                        user=value->as_string().cstr();
                     74:                                } else if(key=="password") {
                     75:                                        password=value->as_string().cstr();
                     76:                                } else if(key=="db") {
                     77:                                        db=r.process(*value).as_int();
                     78:                                } else if(key=="protocol") {
                     79:                                        protocol=r.process(*value).as_int();
                     80:                                        if(protocol!=2 && protocol!=3)
                     81:                                                throw Exception(PARSER_RUNTIME, 0, "protocol must be 2 or 3");
                     82:                                } else if(key=="auto_reconnect") {
                     83:                                        auto_reconnect=r.process(*value).as_int();
1.6     ! moko       84:                                } else if(key=="tls") {
        !            85: #ifdef WITH_REDIS_SSL
        !            86:                                        tls_specified=true;
        !            87:                                        if(HashStringValue* tls_options=value->get_hash())
        !            88:                                                for(HashStringValue::Iterator t(*tls_options); t; t.next()) {
        !            89:                                                        String::Body tkey=t.key();
        !            90:                                                        Value* tvalue=t.value();
        !            91:                                                        if(tkey=="ca") {
        !            92:                                                                tls_ca=tvalue->as_string().cstr();
        !            93:                                                        } else if(tkey=="cert") {
        !            94:                                                                tls_cert=tvalue->as_string().cstr();
        !            95:                                                        } else if(tkey=="key") {
        !            96:                                                                tls_key=tvalue->as_string().cstr();
        !            97:                                                        } else if(tkey=="verify") {
        !            98:                                                                tls_verify=r.process(*tvalue).as_bool();
        !            99:                                                        } else
        !           100:                                                                throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION);
        !           101:                                                }
        !           102: #else
        !           103:                                        throw Exception("redis", 0, "compiled without redis SSL support");
        !           104: #endif
1.1       moko      105:                                } else
                    106:                                        throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION);
                    107:                        }
                    108:                }
                    109: 
                    110:        if(unix_socket && tcp_specified)
                    111:                throw Exception(PARSER_RUNTIME, 0, "unix_socket can not be used with host or port");
                    112: 
                    113:        redisOptions options;
                    114:        memset(&options, 0, sizeof(options));
                    115:        if(unix_socket)
                    116:                REDIS_OPTIONS_SET_UNIX(&options, unix_socket);
                    117:        else
                    118:                REDIS_OPTIONS_SET_TCP(&options, host, port);
                    119: 
                    120:        struct timeval tv;
                    121:        if(timeout>0) { // 0 is none
                    122:                tv.tv_sec=(long)timeout;
                    123:                tv.tv_usec=(long)((timeout-tv.tv_sec)*1000000);
                    124:                options.connect_timeout=&tv;
                    125:                options.command_timeout=&tv;
                    126:        }
                    127: 
                    128:        // ^r.open[] of an opened object: close the old connection first
1.6     ! moko      129:        self.release();
1.1       moko      130: 
                    131:        self.fuser=user;
                    132:        self.fpassword=password;
                    133:        self.fdb=db;
                    134:        self.fprotocol=protocol;
                    135:        self.fauto_reconnect=auto_reconnect;
                    136: 
                    137:        try {
1.6     ! moko      138: #ifdef WITH_REDIS_SSL
        !           139:                if(tls_specified) {
        !           140:                        static bool ssl_initialized=false; // OpenSSL before 1.1 needs it once
        !           141:                        if(!ssl_initialized) {
        !           142:                                redisInitOpenSSL();
        !           143:                                ssl_initialized=true;
        !           144:                        }
        !           145: 
        !           146:                        redisSSLOptions ssl_options;
        !           147:                        memset(&ssl_options, 0, sizeof(ssl_options));
        !           148:                        ssl_options.cacert_filename=tls_ca;
        !           149:                        ssl_options.cert_filename=tls_cert;
        !           150:                        ssl_options.private_key_filename=tls_key;
        !           151:                        ssl_options.server_name=host;
        !           152:                        // as amqp: the server certificate is checked only against the given ca
        !           153:                        ssl_options.verify_mode=tls_verify && tls_ca ? REDIS_SSL_VERIFY_PEER : REDIS_SSL_VERIFY_NONE;
        !           154: 
        !           155:                        redisSSLContextError error=REDIS_SSL_CTX_NONE;
        !           156:                        if(!(self.fssl_context=redisCreateSSLContextWithOptions(&ssl_options, &error)))
        !           157:                                throw Exception("redis", 0, "SSL context failed: %s", redisSSLContextGetError(error));
        !           158:                }
        !           159: #endif
        !           160: 
        !           161:                if(!(self.fcontext=redisConnectWithOptions(&options)))
        !           162:                        throw Exception("redis", 0, "connect failed: out of memory");
        !           163:                if(self.fcontext->err)
        !           164:                        throw Exception("redis", 0, "connect failed: %s", self.fcontext->errstr);
        !           165: 
1.1       moko      166:                self.handshake();
                    167:        } catch(...) {
1.6     ! moko      168:                self.release();
1.1       moko      169:                throw;
                    170:        }
                    171: }
                    172: 
                    173: static void _release(Request& r, MethodParams&) {
1.6     ! moko      174:        GET_SELF(r, VRedis).release();
1.1       moko      175: }
                    176: 
1.4       moko      177: static Value* reply_value(redisReply& reply, bool as_file, const String& command) {
1.1       moko      178:        switch(reply.type) {
                    179:                case REDIS_REPLY_STRING:
                    180:                case REDIS_REPLY_STATUS:
                    181:                case REDIS_REPLY_VERB:
                    182:                case REDIS_REPLY_BIGNUM: {
                    183:                        char* data=pa_strdup(reply.str, reply.len);
1.4       moko      184:                        if(as_file) {
1.1       moko      185:                                VFile& file=*new VFile;
                    186:                                file.set_binary(true/*tainted*/, data, reply.len);
                    187:                                return &file;
                    188:                        }
                    189:                        return new VString(*new String(String::C(data, reply.len), String::L_TAINTED));
                    190:                }
                    191:                case REDIS_REPLY_INTEGER:
                    192:                        if(reply.integer>=PA_WINT_MIN && reply.integer<=PA_WINT_MAX)
                    193:                                return new VInt(reply.integer);
                    194:                        // beyond the precise int range
                    195:                        return new VString(*new String(pa_itoa(reply.integer)));
                    196:                case REDIS_REPLY_DOUBLE:
                    197:                        return new VDouble(reply.dval);
                    198:                case REDIS_REPLY_BOOL:
                    199:                        return &VBool::get(reply.integer!=0);
                    200:                case REDIS_REPLY_NIL:
                    201:                        return VVoid::get();
                    202:                case REDIS_REPLY_ARRAY:
                    203:                case REDIS_REPLY_SET:
                    204:                case REDIS_REPLY_PUSH: {
                    205:                        VArray& result=*new VArray(reply.elements);
                    206:                        ArrayValue& array=result.array();
                    207:                        for(size_t i=0; i<reply.elements; i++)
1.4       moko      208:                                array+=reply_value(*reply.element[i], as_file, command);
1.1       moko      209:                        return &result;
                    210:                }
                    211:                case REDIS_REPLY_MAP: {
                    212:                        VHash& result=*new VHash;
                    213:                        for(size_t i=0; i+1<reply.elements; i+=2)
1.4       moko      214:                                result.hash().put(reply_value(*reply.element[i], false, command)->as_string(), reply_value(*reply.element[i+1], as_file, command));
1.1       moko      215:                        return &result;
                    216:                }
                    217:                case REDIS_REPLY_ERROR:
                    218:                        throw Exception("redis", &command, "%s", reply.str);
                    219:                case REDIS_REPLY_ATTR:
                    220:                        return VVoid::get();
                    221:        }
                    222:        throw Exception("redis", &command, "unknown reply type %d", reply.type);
                    223: }
                    224: 
                    225: // a string, a number, a bool or a file
                    226: static void add_argument(Array<const char*>& argv, Array<size_t>& lengths, Value& value) {
                    227:        if(VFile* file=dynamic_cast<VFile*>(&value)) {
                    228:                argv+=file->value_ptr();
                    229:                lengths+=file->value_size();
                    230:        } else if(dynamic_cast<VBool*>(&value)) {
                    231:                // as redis itself encodes booleans in integer replies
                    232:                argv+=value.as_bool() ? "1" : "0";
                    233:                lengths+=1;
                    234:        } else if(dynamic_cast<VArray*>(&value) || dynamic_cast<VHash*>(&value)) {
                    235:                throw Exception(PARSER_RUNTIME, 0, "redis command argument can not be a nested array or a hash");
                    236:        } else {
                    237:                const char* string=value.as_string().cstr();
                    238:                argv+=string;
                    239:                lengths+=strlen(string);
                    240:        }
                    241: }
                    242: 
                    243: // arguments start at params[first]: strings, numbers, files, not nested arrays and hashes as key value pairs
1.4       moko      244: static void redis_command(Request& r, MethodParams& params, const String& command, size_t first, bool as_file) {
1.1       moko      245:        VRedis& self=GET_SELF(r, VRedis);
                    246: 
                    247:        Array<const char*> argv;
                    248:        Array<size_t> lengths;
                    249:        const char* command_cstr=command.cstr();
                    250:        argv+=command_cstr;
                    251:        lengths+=strlen(command_cstr);
                    252: 
                    253:        for(size_t i=first; i<params.count(); i++) {
                    254:                Value& param=params[i];
                    255:                if(VArray* array=dynamic_cast<VArray*>(&param)) {
                    256:                        for(ArrayValue::Iterator e(array->array()); e; e.next())
                    257:                                if(e.value())
                    258:                                        add_argument(argv, lengths, *e.value());
                    259:                } else if(VHash* hash=dynamic_cast<VHash*>(&param)) {
                    260:                        for(HashStringValue::Iterator e(hash->hash()); e; e.next()) {
                    261:                                const char* key=e.key().cstr();
                    262:                                argv+=key;
                    263:                                lengths+=strlen(key);
                    264:                                add_argument(argv, lengths, *e.value());
                    265:                        }
                    266:                } else
                    267:                        add_argument(argv, lengths, param);
                    268:        }
                    269: 
                    270:        redisReply* reply=self.command((int)argv.count(), argv.ptr(0), lengths.ptr(0));
                    271:        Temp_redis_reply temp_reply(reply);
                    272: 
1.4       moko      273:        r.write(*reply_value(*reply, as_file, command));
1.1       moko      274: }
                    275: 
                    276: static void _call(Request& r, MethodParams& params) {
                    277:        redis_command(r, params, params.as_string(0, "command must be string"), 1, false);
                    278: }
                    279: 
1.4       moko      280: static void _file_call(Request& r, MethodParams& params) {
1.1       moko      281:        redis_command(r, params, params.as_string(0, "command must be string"), 1, true);
                    282: }
                    283: 
                    284: // ^r.command_name[arguments...]: the command is the name of the method called, see VRedis::get_element4call
                    285: void redis_command_method(Request& r, MethodParams& params) {
1.4       moko      286:        const String& name=*r.get_method_frame()->method.name;
                    287:        if(name.starts_with(FILE_PREFIX)) {
                    288:                size_t prefix_length=strlen(FILE_PREFIX);
                    289:                if(name.length()==prefix_length)
                    290:                        throw Exception(PARSER_RUNTIME, &name, "command name expected after " FILE_PREFIX);
                    291:                redis_command(r, params, name.mid(prefix_length, name.length()), 0, true);
                    292:        } else
                    293:                redis_command(r, params, name, 0, false);
1.1       moko      294: }
                    295: 
                    296: #else
                    297: 
                    298: static void _open(Request&, MethodParams&) {
                    299:        throw Exception("redis", 0, "compiled without redis support");
                    300: }
                    301: 
                    302: #endif // WITH_REDIS
                    303: 
                    304: MRedis::MRedis(): Methoded("redis") {
1.6     ! moko      305:        // ^redis::open[ $.host[localhost] $.port(6379) $.unix_socket[path] $.user[] $.password[] $.db(0) $.timeout(seconds, 0 is none) $.protocol(3) $.auto_reconnect(seconds)
        !           306:        //      $.tls[ $.ca[path] $.cert[path] $.key[path] $.verify(true) ] ]
1.1       moko      307:        add_native_method("open", Method::CT_DYNAMIC, _open, 0, 1);
                    308: 
                    309: #ifdef WITH_REDIS
                    310:        // ^r.call[command;arguments...]
                    311:        add_native_method("call", Method::CT_DYNAMIC, _call, 1, MAX_REDIS_COMMAND_PARAMS);
                    312: 
1.5       moko      313:        // ^r.f-call[command;arguments...]
1.4       moko      314:        add_native_method(FILE_PREFIX "call", Method::CT_DYNAMIC, _file_call, 1, MAX_REDIS_COMMAND_PARAMS);
1.1       moko      315: 
                    316:        // ^r.release[]
                    317:        add_native_method("release", Method::CT_DYNAMIC, _release, 0, 0);
                    318: #endif // WITH_REDIS
                    319: }

E-mail: