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