xref: /freebsd/contrib/unbound/cachedb/redis.c (revision 0957b409)
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 redisContext*
63 redis_connect(const struct redis_moddata* moddata)
64 {
65 	redisContext* ctx;
66 
67 	ctx = redisConnectWithTimeout(moddata->server_host,
68 		moddata->server_port, moddata->timeout);
69 	if(!ctx || ctx->err) {
70 		const char *errstr = "out of memory";
71 		if(ctx)
72 			errstr = ctx->errstr;
73 		log_err("failed to connect to redis server: %s", errstr);
74 		goto fail;
75 	}
76 	if(redisSetTimeout(ctx, moddata->timeout) != REDIS_OK) {
77 		log_err("failed to set redis timeout");
78 		goto fail;
79 	}
80 	return ctx;
81 
82   fail:
83 	if(ctx)
84 		redisFree(ctx);
85 	return NULL;
86 }
87 
88 static int
89 redis_init(struct module_env* env, struct cachedb_env* cachedb_env)
90 {
91 	int i;
92 	struct redis_moddata* moddata = NULL;
93 
94 	verbose(VERB_ALGO, "redis_init");
95 
96 	moddata = calloc(1, sizeof(struct redis_moddata));
97 	if(!moddata) {
98 		log_err("out of memory");
99 		return 0;
100 	}
101 	moddata->numctxs = env->cfg->num_threads;
102 	moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*));
103 	if(!moddata->ctxs) {
104 		log_err("out of memory");
105 		free(moddata);
106 		return 0;
107 	}
108 	/* note: server_host is a shallow reference to configured string.
109 	 * we don't have to free it in this module. */
110 	moddata->server_host = env->cfg->redis_server_host;
111 	moddata->server_port = env->cfg->redis_server_port;
112 	moddata->timeout.tv_sec = env->cfg->redis_timeout / 1000;
113 	moddata->timeout.tv_usec = (env->cfg->redis_timeout % 1000) * 1000;
114 	for(i = 0; i < moddata->numctxs; i++)
115 		moddata->ctxs[i] = redis_connect(moddata);
116 	cachedb_env->backend_data = moddata;
117 	return 1;
118 }
119 
120 static void
121 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env)
122 {
123 	struct redis_moddata* moddata = (struct redis_moddata*)
124 		cachedb_env->backend_data;
125 	(void)env;
126 
127 	verbose(VERB_ALGO, "redis_deinit");
128 
129 	if(!moddata)
130 		return;
131 	if(moddata->ctxs) {
132 		int i;
133 		for(i = 0; i < moddata->numctxs; i++) {
134 			if(moddata->ctxs[i])
135 				redisFree(moddata->ctxs[i]);
136 		}
137 		free(moddata->ctxs);
138 	}
139 	free(moddata);
140 }
141 
142 /*
143  * Send a redis command and get a reply.  Unified so that it can be used for
144  * both SET and GET.  If 'data' is non-NULL the command is supposed to be
145  * SET and GET otherwise, but the implementation of this function is agnostic
146  * about the semantics (except for logging): 'command', 'data', and 'data_len'
147  * are opaquely passed to redisCommand().
148  * This function first checks whether a connection with a redis server has
149  * been established; if not it tries to set up a new one.
150  * It returns redisReply returned from redisCommand() or NULL if some low
151  * level error happens.  The caller is responsible to check the return value,
152  * if it's non-NULL, it has to free it with freeReplyObject().
153  */
154 static redisReply*
155 redis_command(struct module_env* env, struct cachedb_env* cachedb_env,
156 	const char* command, const uint8_t* data, size_t data_len)
157 {
158 	redisContext* ctx;
159 	redisReply* rep;
160 	struct redis_moddata* d = (struct redis_moddata*)
161 		cachedb_env->backend_data;
162 
163 	/* We assume env->alloc->thread_num is a unique ID for each thread
164 	 * in [0, num-of-threads).  We could treat it as an error condition
165 	 * if the assumption didn't hold, but it seems to be a fundamental
166 	 * assumption throughout the unbound architecture, so we simply assert
167 	 * it. */
168 	log_assert(env->alloc->thread_num < d->numctxs);
169 	ctx = d->ctxs[env->alloc->thread_num];
170 
171 	/* If we've not established a connection to the server or we've closed
172 	 * it on a failure, try to re-establish a new one.   Failures will be
173 	 * logged in redis_connect(). */
174 	if(!ctx) {
175 		ctx = redis_connect(d);
176 		d->ctxs[env->alloc->thread_num] = ctx;
177 	}
178 	if(!ctx)
179 		return NULL;
180 
181 	/* Send the command and get a reply, synchronously. */
182 	rep = (redisReply*)redisCommand(ctx, command, data, data_len);
183 	if(!rep) {
184 		/* Once an error as a NULL-reply is returned the context cannot
185 		 * be reused and we'll need to set up a new connection. */
186 		log_err("redis_command: failed to receive a reply, "
187 			"closing connection: %s", ctx->errstr);
188 		redisFree(ctx);
189 		d->ctxs[env->alloc->thread_num] = NULL;
190 		return NULL;
191 	}
192 
193 	/* Check error in reply to unify logging in that case.
194 	 * The caller may perform context-dependent checks and logging. */
195 	if(rep->type == REDIS_REPLY_ERROR)
196 		log_err("redis: %s resulted in an error: %s",
197 			data ? "set" : "get", rep->str);
198 
199 	return rep;
200 }
201 
202 static int
203 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env,
204 	char* key, struct sldns_buffer* result_buffer)
205 {
206 	redisReply* rep;
207 	char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */
208 	int n;
209 	int ret = 0;
210 
211 	verbose(VERB_ALGO, "redis_lookup of %s", key);
212 
213 	n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key);
214 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
215 		log_err("redis_lookup: unexpected failure to build command");
216 		return 0;
217 	}
218 
219 	rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0);
220 	if(!rep)
221 		return 0;
222 	switch (rep->type) {
223 	case REDIS_REPLY_NIL:
224 		verbose(VERB_ALGO, "redis_lookup: no data cached");
225 		break;
226 	case REDIS_REPLY_STRING:
227 		verbose(VERB_ALGO, "redis_lookup found %d bytes",
228 			(int)rep->len);
229 		if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) {
230 			log_err("redis_lookup: replied data too long: %lu",
231 				(size_t)rep->len);
232 			break;
233 		}
234 		sldns_buffer_clear(result_buffer);
235 		sldns_buffer_write(result_buffer, rep->str, rep->len);
236 		sldns_buffer_flip(result_buffer);
237 		ret = 1;
238 		break;
239 	case REDIS_REPLY_ERROR:
240 		break;		/* already logged */
241 	default:
242 		log_err("redis_lookup: unexpected type of reply for (%d)",
243 			rep->type);
244 		break;
245 	}
246 	freeReplyObject(rep);
247 	return ret;
248 }
249 
250 static void
251 redis_store(struct module_env* env, struct cachedb_env* cachedb_env,
252 	char* key, uint8_t* data, size_t data_len)
253 {
254 	redisReply* rep;
255 	char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+3+1]; /* "SET " + key + " %b" */
256 	int n;
257 
258 	verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len);
259 
260 	/* build command to set to a binary safe string */
261 	n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key);
262 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
263 		log_err("redis_store: unexpected failure to build command");
264 		return;
265 	}
266 
267 	rep = redis_command(env, cachedb_env, cmdbuf, data, data_len);
268 	if(rep) {
269 		verbose(VERB_ALGO, "redis_store set completed");
270 		if(rep->type != REDIS_REPLY_STATUS &&
271 			rep->type != REDIS_REPLY_ERROR) {
272 			log_err("redis_store: unexpected type of reply (%d)",
273 				rep->type);
274 		}
275 		freeReplyObject(rep);
276 	}
277 }
278 
279 struct cachedb_backend redis_backend = { "redis",
280 	redis_init, redis_deinit, redis_lookup, redis_store
281 };
282 #endif	/* USE_REDIS */
283 #endif /* USE_CACHEDB */
284