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*>(¶m)) {
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*>(¶m)) {
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: