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	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
65static redisReply* redis_command(struct module_env*, struct cachedb_env*,
66	const char*, const uint8_t*, size_t);
67
68static void
69moddata_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
84static redisContext*
85redis_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
131fail:
132	if(ctx)
133		redisFree(ctx);
134	return NULL;
135}
136
137static int
138redis_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
202fail:
203	moddata_clean(&moddata);
204	return 0;
205}
206
207static void
208redis_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 */
230static redisReply*
231redis_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
278static int
279redis_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
326static void
327redis_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
372struct cachedb_backend redis_backend = { "redis",
373	redis_init, redis_deinit, redis_lookup, redis_store
374};
375#endif	/* USE_REDIS */
376#endif /* USE_CACHEDB */
377