xref: /freebsd/contrib/unbound/cachedb/redis.c (revision 7fdf597e96a02165cfe22ff357b857d5fa15ed8a)
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 	const char* server_path; /* server's unix path, or "", NULL if unused */
60 	const char* server_password; /* server's AUTH password, or "", NULL if unused */
61 	struct timeval command_timeout;	 /* timeout for commands */
62 	struct timeval connect_timeout;	 /* timeout for connect */
63 	int logical_db;		/* the redis logical database to use */
64 };
65 
66 static redisReply* redis_command(struct module_env*, struct cachedb_env*,
67 	const char*, const uint8_t*, size_t);
68 
69 static void
70 moddata_clean(struct redis_moddata** moddata) {
71 	if(!moddata || !*moddata)
72 		return;
73 	if((*moddata)->ctxs) {
74 		int i;
75 		for(i = 0; i < (*moddata)->numctxs; i++) {
76 			if((*moddata)->ctxs[i])
77 				redisFree((*moddata)->ctxs[i]);
78 		}
79 		free((*moddata)->ctxs);
80 	}
81 	free(*moddata);
82 	*moddata = NULL;
83 }
84 
85 static redisContext*
86 redis_connect(const struct redis_moddata* moddata)
87 {
88 	redisContext* ctx;
89 
90 	if(moddata->server_path && moddata->server_path[0]!=0) {
91 		ctx = redisConnectUnixWithTimeout(moddata->server_path,
92 			moddata->connect_timeout);
93 	} else {
94 		ctx = redisConnectWithTimeout(moddata->server_host,
95 			moddata->server_port, moddata->connect_timeout);
96 	}
97 	if(!ctx || ctx->err) {
98 		const char *errstr = "out of memory";
99 		if(ctx)
100 			errstr = ctx->errstr;
101 		log_err("failed to connect to redis server: %s", errstr);
102 		goto fail;
103 	}
104 	if(redisSetTimeout(ctx, moddata->command_timeout) != REDIS_OK) {
105 		log_err("failed to set redis timeout");
106 		goto fail;
107 	}
108 	if(moddata->server_password && moddata->server_password[0]!=0) {
109 		redisReply* rep;
110 		rep = redisCommand(ctx, "AUTH %s", moddata->server_password);
111 		if(!rep || rep->type == REDIS_REPLY_ERROR) {
112 			log_err("failed to authenticate with password");
113 			freeReplyObject(rep);
114 			goto fail;
115 		}
116 		freeReplyObject(rep);
117 	}
118 	if(moddata->logical_db > 0) {
119 		redisReply* rep;
120 		rep = redisCommand(ctx, "SELECT %d", moddata->logical_db);
121 		if(!rep || rep->type == REDIS_REPLY_ERROR) {
122 			log_err("failed to set logical database (%d)",
123 				moddata->logical_db);
124 			freeReplyObject(rep);
125 			goto fail;
126 		}
127 		freeReplyObject(rep);
128 	}
129 	verbose(VERB_OPS, "Connection to Redis established");
130 	return ctx;
131 
132 fail:
133 	if(ctx)
134 		redisFree(ctx);
135 	return NULL;
136 }
137 
138 static int
139 redis_init(struct module_env* env, struct cachedb_env* cachedb_env)
140 {
141 	int i;
142 	struct redis_moddata* moddata = NULL;
143 
144 	verbose(VERB_OPS, "Redis initialization");
145 
146 	moddata = calloc(1, sizeof(struct redis_moddata));
147 	if(!moddata) {
148 		log_err("out of memory");
149 		goto fail;
150 	}
151 	moddata->numctxs = env->cfg->num_threads;
152 	moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*));
153 	if(!moddata->ctxs) {
154 		log_err("out of memory");
155 		goto fail;
156 	}
157 	/* note: server_host is a shallow reference to configured string.
158 	 * we don't have to free it in this module. */
159 	moddata->server_host = env->cfg->redis_server_host;
160 	moddata->server_port = env->cfg->redis_server_port;
161 	moddata->server_path = env->cfg->redis_server_path;
162 	moddata->server_password = env->cfg->redis_server_password;
163 	moddata->command_timeout.tv_sec = env->cfg->redis_timeout / 1000;
164 	moddata->command_timeout.tv_usec =
165 		(env->cfg->redis_timeout % 1000) * 1000;
166 	moddata->connect_timeout.tv_sec = env->cfg->redis_timeout / 1000;
167 	moddata->connect_timeout.tv_usec =
168 		(env->cfg->redis_timeout % 1000) * 1000;
169 	if(env->cfg->redis_command_timeout != 0) {
170 		moddata->command_timeout.tv_sec =
171 			env->cfg->redis_command_timeout / 1000;
172 		moddata->command_timeout.tv_usec =
173 			(env->cfg->redis_command_timeout % 1000) * 1000;
174 	}
175 	if(env->cfg->redis_connect_timeout != 0) {
176 		moddata->connect_timeout.tv_sec =
177 			env->cfg->redis_connect_timeout / 1000;
178 		moddata->connect_timeout.tv_usec =
179 			(env->cfg->redis_connect_timeout % 1000) * 1000;
180 	}
181 	moddata->logical_db = env->cfg->redis_logical_db;
182 	for(i = 0; i < moddata->numctxs; i++) {
183 		redisContext* ctx = redis_connect(moddata);
184 		if(!ctx) {
185 			log_err("redis_init: failed to init redis");
186 			goto fail;
187 		}
188 		moddata->ctxs[i] = ctx;
189 	}
190 	cachedb_env->backend_data = moddata;
191 	if(env->cfg->redis_expire_records) {
192 		redisReply* rep = NULL;
193 		int redis_reply_type = 0;
194 		/** check if setex command is supported */
195 		rep = redis_command(env, cachedb_env,
196 			"SETEX __UNBOUND_REDIS_CHECK__ 1 none", NULL, 0);
197 		if(!rep) {
198 			/** init failed, no response from redis server*/
199 			log_err("redis_init: failed to init redis, the "
200 				"redis-expire-records option requires the SETEX command "
201 				"(redis >= 2.0.0)");
202 			goto fail;
203 		}
204 		redis_reply_type = rep->type;
205 		freeReplyObject(rep);
206 		switch(redis_reply_type) {
207 		case REDIS_REPLY_STATUS:
208 			break;
209 		default:
210 			/** init failed, setex command not supported */
211 			log_err("redis_init: failed to init redis, the "
212 				"redis-expire-records option requires the SETEX command "
213 				"(redis >= 2.0.0)");
214 			goto fail;
215 		}
216 	}
217 	return 1;
218 
219 fail:
220 	moddata_clean(&moddata);
221 	return 0;
222 }
223 
224 static void
225 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env)
226 {
227 	struct redis_moddata* moddata = (struct redis_moddata*)
228 		cachedb_env->backend_data;
229 	(void)env;
230 
231 	verbose(VERB_OPS, "Redis deinitialization");
232 	moddata_clean(&moddata);
233 }
234 
235 /*
236  * Send a redis command and get a reply.  Unified so that it can be used for
237  * both SET and GET.  If 'data' is non-NULL the command is supposed to be
238  * SET and GET otherwise, but the implementation of this function is agnostic
239  * about the semantics (except for logging): 'command', 'data', and 'data_len'
240  * are opaquely passed to redisCommand().
241  * This function first checks whether a connection with a redis server has
242  * been established; if not it tries to set up a new one.
243  * It returns redisReply returned from redisCommand() or NULL if some low
244  * level error happens.  The caller is responsible to check the return value,
245  * if it's non-NULL, it has to free it with freeReplyObject().
246  */
247 static redisReply*
248 redis_command(struct module_env* env, struct cachedb_env* cachedb_env,
249 	const char* command, const uint8_t* data, size_t data_len)
250 {
251 	redisContext* ctx;
252 	redisReply* rep;
253 	struct redis_moddata* d = (struct redis_moddata*)
254 		cachedb_env->backend_data;
255 
256 	/* We assume env->alloc->thread_num is a unique ID for each thread
257 	 * in [0, num-of-threads).  We could treat it as an error condition
258 	 * if the assumption didn't hold, but it seems to be a fundamental
259 	 * assumption throughout the unbound architecture, so we simply assert
260 	 * it. */
261 	log_assert(env->alloc->thread_num < d->numctxs);
262 	ctx = d->ctxs[env->alloc->thread_num];
263 
264 	/* If we've not established a connection to the server or we've closed
265 	 * it on a failure, try to re-establish a new one.   Failures will be
266 	 * logged in redis_connect(). */
267 	if(!ctx) {
268 		ctx = redis_connect(d);
269 		d->ctxs[env->alloc->thread_num] = ctx;
270 	}
271 	if(!ctx)
272 		return NULL;
273 
274 	/* Send the command and get a reply, synchronously. */
275 	rep = (redisReply*)redisCommand(ctx, command, data, data_len);
276 	if(!rep) {
277 		/* Once an error as a NULL-reply is returned the context cannot
278 		 * be reused and we'll need to set up a new connection. */
279 		log_err("redis_command: failed to receive a reply, "
280 			"closing connection: %s", ctx->errstr);
281 		redisFree(ctx);
282 		d->ctxs[env->alloc->thread_num] = NULL;
283 		return NULL;
284 	}
285 
286 	/* Check error in reply to unify logging in that case.
287 	 * The caller may perform context-dependent checks and logging. */
288 	if(rep->type == REDIS_REPLY_ERROR)
289 		log_err("redis: %s resulted in an error: %s",
290 			data ? "set" : "get", rep->str);
291 
292 	return rep;
293 }
294 
295 static int
296 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env,
297 	char* key, struct sldns_buffer* result_buffer)
298 {
299 	redisReply* rep;
300 	char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */
301 	int n;
302 	int ret = 0;
303 
304 	verbose(VERB_ALGO, "redis_lookup of %s", key);
305 
306 	n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key);
307 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
308 		log_err("redis_lookup: unexpected failure to build command");
309 		return 0;
310 	}
311 
312 	rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0);
313 	if(!rep)
314 		return 0;
315 	switch(rep->type) {
316 	case REDIS_REPLY_NIL:
317 		verbose(VERB_ALGO, "redis_lookup: no data cached");
318 		break;
319 	case REDIS_REPLY_STRING:
320 		verbose(VERB_ALGO, "redis_lookup found %d bytes",
321 			(int)rep->len);
322 		if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) {
323 			log_err("redis_lookup: replied data too long: %lu",
324 				(size_t)rep->len);
325 			break;
326 		}
327 		sldns_buffer_clear(result_buffer);
328 		sldns_buffer_write(result_buffer, rep->str, rep->len);
329 		sldns_buffer_flip(result_buffer);
330 		ret = 1;
331 		break;
332 	case REDIS_REPLY_ERROR:
333 		break;		/* already logged */
334 	default:
335 		log_err("redis_lookup: unexpected type of reply for (%d)",
336 			rep->type);
337 		break;
338 	}
339 	freeReplyObject(rep);
340 	return ret;
341 }
342 
343 static void
344 redis_store(struct module_env* env, struct cachedb_env* cachedb_env,
345 	char* key, uint8_t* data, size_t data_len, time_t ttl)
346 {
347 	redisReply* rep;
348 	int n;
349 	int set_ttl = (env->cfg->redis_expire_records &&
350 		(!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0));
351 	/* Supported commands:
352 	 * - "SET " + key + " %b"
353 	 * - "SETEX " + key + " " + ttl + " %b"
354 	 */
355 	char cmdbuf[6+(CACHEDB_HASHSIZE/8)*2+11+3+1];
356 
357 	if (!set_ttl) {
358 		verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len);
359 		/* build command to set to a binary safe string */
360 		n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key);
361 	} else {
362 		/* add expired ttl time to redis ttl to avoid premature eviction of key */
363 		ttl += env->cfg->serve_expired_ttl;
364 		verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u",
365 			key, (int)data_len, (uint32_t)ttl);
366 		/* build command to set to a binary safe string */
367 		n = snprintf(cmdbuf, sizeof(cmdbuf), "SETEX %s %u %%b", key,
368 			(uint32_t)ttl);
369 	}
370 
371 
372 	if(n < 0 || n >= (int)sizeof(cmdbuf)) {
373 		log_err("redis_store: unexpected failure to build command");
374 		return;
375 	}
376 
377 	rep = redis_command(env, cachedb_env, cmdbuf, data, data_len);
378 	if(rep) {
379 		verbose(VERB_ALGO, "redis_store set completed");
380 		if(rep->type != REDIS_REPLY_STATUS &&
381 			rep->type != REDIS_REPLY_ERROR) {
382 			log_err("redis_store: unexpected type of reply (%d)",
383 				rep->type);
384 		}
385 		freeReplyObject(rep);
386 	}
387 }
388 
389 struct cachedb_backend redis_backend = { "redis",
390 	redis_init, redis_deinit, redis_lookup, redis_store
391 };
392 #endif	/* USE_REDIS */
393 #endif /* USE_CACHEDB */
394