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