]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - contrib/unbound/cachedb/redis.c
MFV 364468:
[FreeBSD/FreeBSD.git] / contrib / unbound / cachedb / redis.c
1 /*
2  * cachedb/redis.c - cachedb redis module
3  *
4  * Copyright (c) 2018, NLnet Labs. All rights reserved.
5  *
6  * This software is open source.
7  * 
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  * 
12  * Redistributions of source code must retain the above copyright notice,
13  * this list of conditions and the following disclaimer.
14  * 
15  * Redistributions in binary form must reproduce the above copyright notice,
16  * this list of conditions and the following disclaimer in the documentation
17  * and/or other materials provided with the distribution.
18  * 
19  * Neither the name of the NLNET LABS nor the names of its contributors may
20  * be used to endorse or promote products derived from this software without
21  * specific prior written permission.
22  * 
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27  * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
29  * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34  */
35
36 /**
37  * \file
38  *
39  * This file contains a module that uses the redis database to cache
40  * dns responses.
41  */
42
43 #include "config.h"
44 #ifdef USE_CACHEDB
45 #include "cachedb/redis.h"
46 #include "cachedb/cachedb.h"
47 #include "util/alloc.h"
48 #include "util/config_file.h"
49 #include "sldns/sbuffer.h"
50
51 #ifdef USE_REDIS
52 #include "hiredis/hiredis.h"
53
54 struct redis_moddata {
55         redisContext** ctxs;    /* thread-specific redis contexts */
56         int numctxs;            /* number of ctx entries */
57         const char* server_host; /* server's IP address or host name */
58         int server_port;         /* server's TCP port */
59         struct timeval timeout;  /* timeout for connection setup and commands */
60 };
61
62 static redisReply* redis_command(struct module_env*, struct cachedb_env*,
63         const char*, const uint8_t*, size_t);
64
65 static redisContext*
66 redis_connect(const struct redis_moddata* moddata)
67 {
68         redisContext* ctx;
69
70         ctx = redisConnectWithTimeout(moddata->server_host,
71                 moddata->server_port, moddata->timeout);
72         if(!ctx || ctx->err) {
73                 const char *errstr = "out of memory";
74                 if(ctx)
75                         errstr = ctx->errstr;
76                 log_err("failed to connect to redis server: %s", errstr);
77                 goto fail;
78         }
79         if(redisSetTimeout(ctx, moddata->timeout) != REDIS_OK) {
80                 log_err("failed to set redis timeout");
81                 goto fail;
82         }
83         return ctx;
84
85   fail:
86         if(ctx)
87                 redisFree(ctx);
88         return NULL;
89 }
90
91 static int
92 redis_init(struct module_env* env, struct cachedb_env* cachedb_env)
93 {
94         int i;
95         struct redis_moddata* moddata = NULL;
96
97         verbose(VERB_ALGO, "redis_init");
98
99         moddata = calloc(1, sizeof(struct redis_moddata));
100         if(!moddata) {
101                 log_err("out of memory");
102                 return 0;
103         }
104         moddata->numctxs = env->cfg->num_threads;
105         moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*));
106         if(!moddata->ctxs) {
107                 log_err("out of memory");
108                 free(moddata);
109                 return 0;
110         }
111         /* note: server_host is a shallow reference to configured string.
112          * we don't have to free it in this module. */
113         moddata->server_host = env->cfg->redis_server_host;
114         moddata->server_port = env->cfg->redis_server_port;
115         moddata->timeout.tv_sec = env->cfg->redis_timeout / 1000;
116         moddata->timeout.tv_usec = (env->cfg->redis_timeout % 1000) * 1000;
117         for(i = 0; i < moddata->numctxs; i++)
118                 moddata->ctxs[i] = redis_connect(moddata);
119         cachedb_env->backend_data = moddata;
120         if(env->cfg->redis_expire_records) {
121                 redisReply* rep = NULL;
122                 int redis_reply_type = 0;
123                 /** check if setex command is supported */
124                 rep = redis_command(env, cachedb_env,
125                         "SETEX __UNBOUND_REDIS_CHECK__ 1 none", NULL, 0);
126                 if(!rep) {
127                         /** init failed, no response from redis server*/
128                         log_err("redis_init: failed to init redis, the "
129                                 "redis-expire-records option requires the SETEX command "
130                                 "(redis >= 2.0.0)");
131                         return 0;
132                 }
133                 redis_reply_type = rep->type;
134                 freeReplyObject(rep);
135                 switch(redis_reply_type) {
136                 case REDIS_REPLY_STATUS:
137                         break;
138                 default:
139                         /** init failed, setex command not supported */
140                         log_err("redis_init: failed to init redis, the "
141                                 "redis-expire-records option requires the SETEX command "
142                                 "(redis >= 2.0.0)");
143                         return 0;
144                 }
145         }
146
147         return 1;
148 }
149
150 static void
151 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env)
152 {
153         struct redis_moddata* moddata = (struct redis_moddata*)
154                 cachedb_env->backend_data;
155         (void)env;
156
157         verbose(VERB_ALGO, "redis_deinit");
158
159         if(!moddata)
160                 return;
161         if(moddata->ctxs) {
162                 int i;
163                 for(i = 0; i < moddata->numctxs; i++) {
164                         if(moddata->ctxs[i])
165                                 redisFree(moddata->ctxs[i]);
166                 }
167                 free(moddata->ctxs);
168         }
169         free(moddata);
170 }
171
172 /*
173  * Send a redis command and get a reply.  Unified so that it can be used for
174  * both SET and GET.  If 'data' is non-NULL the command is supposed to be
175  * SET and GET otherwise, but the implementation of this function is agnostic
176  * about the semantics (except for logging): 'command', 'data', and 'data_len'
177  * are opaquely passed to redisCommand().
178  * This function first checks whether a connection with a redis server has
179  * been established; if not it tries to set up a new one.
180  * It returns redisReply returned from redisCommand() or NULL if some low
181  * level error happens.  The caller is responsible to check the return value,
182  * if it's non-NULL, it has to free it with freeReplyObject().
183  */
184 static redisReply*
185 redis_command(struct module_env* env, struct cachedb_env* cachedb_env,
186         const char* command, const uint8_t* data, size_t data_len)
187 {
188         redisContext* ctx;
189         redisReply* rep;
190         struct redis_moddata* d = (struct redis_moddata*)
191                 cachedb_env->backend_data;
192
193         /* We assume env->alloc->thread_num is a unique ID for each thread
194          * in [0, num-of-threads).  We could treat it as an error condition
195          * if the assumption didn't hold, but it seems to be a fundamental
196          * assumption throughout the unbound architecture, so we simply assert
197          * it. */
198         log_assert(env->alloc->thread_num < d->numctxs);
199         ctx = d->ctxs[env->alloc->thread_num];
200
201         /* If we've not established a connection to the server or we've closed
202          * it on a failure, try to re-establish a new one.   Failures will be
203          * logged in redis_connect(). */
204         if(!ctx) {
205                 ctx = redis_connect(d);
206                 d->ctxs[env->alloc->thread_num] = ctx;
207         }
208         if(!ctx)
209                 return NULL;
210
211         /* Send the command and get a reply, synchronously. */
212         rep = (redisReply*)redisCommand(ctx, command, data, data_len);
213         if(!rep) {
214                 /* Once an error as a NULL-reply is returned the context cannot
215                  * be reused and we'll need to set up a new connection. */
216                 log_err("redis_command: failed to receive a reply, "
217                         "closing connection: %s", ctx->errstr);
218                 redisFree(ctx);
219                 d->ctxs[env->alloc->thread_num] = NULL;
220                 return NULL;
221         }
222
223         /* Check error in reply to unify logging in that case.
224          * The caller may perform context-dependent checks and logging. */
225         if(rep->type == REDIS_REPLY_ERROR)
226                 log_err("redis: %s resulted in an error: %s",
227                         data ? "set" : "get", rep->str);
228
229         return rep;
230 }
231
232 static int
233 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env,
234         char* key, struct sldns_buffer* result_buffer)
235 {
236         redisReply* rep;
237         char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */
238         int n;
239         int ret = 0;
240
241         verbose(VERB_ALGO, "redis_lookup of %s", key);
242
243         n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key);
244         if(n < 0 || n >= (int)sizeof(cmdbuf)) {
245                 log_err("redis_lookup: unexpected failure to build command");
246                 return 0;
247         }
248
249         rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0);
250         if(!rep)
251                 return 0;
252         switch(rep->type) {
253         case REDIS_REPLY_NIL:
254                 verbose(VERB_ALGO, "redis_lookup: no data cached");
255                 break;
256         case REDIS_REPLY_STRING:
257                 verbose(VERB_ALGO, "redis_lookup found %d bytes",
258                         (int)rep->len);
259                 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) {
260                         log_err("redis_lookup: replied data too long: %lu",
261                                 (size_t)rep->len);
262                         break;
263                 }
264                 sldns_buffer_clear(result_buffer);
265                 sldns_buffer_write(result_buffer, rep->str, rep->len);
266                 sldns_buffer_flip(result_buffer);
267                 ret = 1;
268                 break;
269         case REDIS_REPLY_ERROR:
270                 break;          /* already logged */
271         default:
272                 log_err("redis_lookup: unexpected type of reply for (%d)",
273                         rep->type);
274                 break;
275         }
276         freeReplyObject(rep);
277         return ret;
278 }
279
280 static void
281 redis_store(struct module_env* env, struct cachedb_env* cachedb_env,
282         char* key, uint8_t* data, size_t data_len, time_t ttl)
283 {
284         redisReply* rep;
285         int n;
286         int set_ttl = (env->cfg->redis_expire_records &&
287                 (!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0));
288         /* Supported commands:
289          * - "SET " + key + " %b"
290          * - "SETEX " + key + " " + ttl + " %b"
291          */
292         char cmdbuf[6+(CACHEDB_HASHSIZE/8)*2+11+3+1];
293
294         if (!set_ttl) {
295                 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len);
296                 /* build command to set to a binary safe string */
297                 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key);
298         } else {
299                 /* add expired ttl time to redis ttl to avoid premature eviction of key */
300                 ttl += env->cfg->serve_expired_ttl;
301                 verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u",
302                         key, (int)data_len, (uint32_t)ttl);
303                 /* build command to set to a binary safe string */
304                 n = snprintf(cmdbuf, sizeof(cmdbuf), "SETEX %s %u %%b", key,
305                         (uint32_t)ttl);
306         }
307
308
309         if(n < 0 || n >= (int)sizeof(cmdbuf)) {
310                 log_err("redis_store: unexpected failure to build command");
311                 return;
312         }
313
314         rep = redis_command(env, cachedb_env, cmdbuf, data, data_len);
315         if(rep) {
316                 verbose(VERB_ALGO, "redis_store set completed");
317                 if(rep->type != REDIS_REPLY_STATUS &&
318                         rep->type != REDIS_REPLY_ERROR) {
319                         log_err("redis_store: unexpected type of reply (%d)",
320                                 rep->type);
321                 }
322                 freeReplyObject(rep);
323         }
324 }
325
326 struct cachedb_backend redis_backend = { "redis",
327         redis_init, redis_deinit, redis_lookup, redis_store
328 };
329 #endif  /* USE_REDIS */
330 #endif /* USE_CACHEDB */