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
54struct 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
62static redisReply* redis_command(struct module_env*, struct cachedb_env*,
63	const char*, const uint8_t*, size_t);
64
65static redisContext*
66redis_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
91static int
92redis_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
150static void
151redis_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 */
184static redisReply*
185redis_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
232static int
233redis_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
280static void
281redis_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
326struct cachedb_backend redis_backend = { "redis",
327	redis_init, redis_deinit, redis_lookup, redis_store
328};
329#endif	/* USE_REDIS */
330#endif /* USE_CACHEDB */
331