1 /*
2 * ProFTPD: mod_wrap2_redis -- a mod_wrap2 sub-module for supplying IP-based
3 * access control data via Redis
4 * Copyright (c) 2017 TJ Saunders
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19 *
20 * As a special exemption, TJ Saunders gives permission to link this program
21 * with OpenSSL, and distribute the resulting executable, without including
22 * the source code for OpenSSL in the source distribution.
23 */
24
25 #include "mod_wrap2.h"
26 #include "redis.h"
27
28 #define MOD_WRAP2_REDIS_VERSION "mod_wrap2_redis/0.1"
29
30 #define WRAP2_REDIS_NKEYS 2
31 #define WRAP2_REDIS_CLIENT_KEY_IDX 0
32 #define WRAP2_REDIS_OPTION_KEY_IDX 1
33
34 module wrap2_redis_module;
35
get_named_key(pool * p,char * key,const char * name)36 static char *get_named_key(pool *p, char *key, const char *name) {
37 if (name == NULL) {
38 return key;
39 }
40
41 if (strstr(key, "%{name}") != NULL) {
42 key = (char *) sreplace(p, key, "%{name}", name, NULL);
43 }
44
45 return key;
46 }
47
redistab_close_cb(wrap2_table_t * redistab)48 static int redistab_close_cb(wrap2_table_t *redistab) {
49 pr_redis_t *redis;
50
51 redis = redistab->tab_handle;
52
53 (void) pr_redis_conn_close(redis);
54 redistab->tab_handle = NULL;
55 return 0;
56 }
57
redistab_fetch_clients_cb(wrap2_table_t * redistab,const char * name)58 static array_header *redistab_fetch_clients_cb(wrap2_table_t *redistab,
59 const char *name) {
60 register unsigned int i;
61 pool *tmp_pool = NULL;
62 pr_redis_t *redis;
63 char *key = NULL, **vals = NULL;
64 array_header *items = NULL, *itemszs = NULL, *clients = NULL;
65 int res, xerrno = 0, use_list = TRUE;
66
67 /* Allocate a temporary pool for the duration of this read. */
68 tmp_pool = make_sub_pool(redistab->tab_pool);
69
70 key = ((char **) redistab->tab_data)[WRAP2_REDIS_CLIENT_KEY_IDX];
71
72 if (strncasecmp(key, "list:", 5) == 0) {
73 key += 5;
74
75 } else if (strncasecmp(key, "set:", 4) == 0) {
76 use_list = FALSE;
77 key += 4;
78 }
79
80 key = get_named_key(tmp_pool, key, name);
81 redis = redistab->tab_handle;
82
83 if (use_list == TRUE) {
84 res = pr_redis_list_getall(tmp_pool, redis, &wrap2_redis_module, key,
85 &items, &itemszs);
86 xerrno = errno;
87
88 } else {
89 res = pr_redis_set_getall(tmp_pool, redis, &wrap2_redis_module, key,
90 &items, &itemszs);
91 xerrno = errno;
92 }
93
94 /* Check the results. */
95 if (res < 0) {
96 if (use_list == TRUE) {
97 wrap2_log("error obtaining clients from Redis using list '%s': %s",
98 key, strerror(xerrno));
99
100 } else {
101 wrap2_log("error obtaining clients from Redis using set '%s': %s",
102 key, strerror(xerrno));
103 }
104
105 destroy_pool(tmp_pool);
106 errno = xerrno;
107 return NULL;
108 }
109
110 if (items->nelts < 1) {
111 if (use_list == TRUE) {
112 wrap2_log("no clients found in Redis using list '%s'", key);
113
114 } else {
115 wrap2_log("no clients found in Redis using set '%s'", key);
116 }
117
118 destroy_pool(tmp_pool);
119 errno = ENOENT;
120 return NULL;
121 }
122
123 clients = make_array(redistab->tab_pool, items->nelts, sizeof(char *));
124
125 /* Iterate through each returned row. If there are commas or whitespace
126 * in the row, parse them as separate client names. Otherwise, a comma-
127 * or space-delimited list of names will be treated as a single name, and
128 * violate the principle of least surprise for the site admin.
129 */
130
131 vals = (char **) items->elts;
132
133 for (i = 0; i < items->nelts; i++) {
134 char *ptr, *val;
135
136 if (vals[i] == NULL) {
137 continue;
138 }
139
140 val = vals[i];
141
142 /* Values in Redis are NOT NUL-terminated. */
143 val = pstrndup(tmp_pool, val, ((size_t *) itemszs->elts)[i]);
144
145 ptr = strpbrk(val, ", \t");
146 if (ptr != NULL) {
147 char *dup_opts, *word;
148
149 dup_opts = pstrdup(redistab->tab_pool, val);
150 while ((word = pr_str_get_token(&dup_opts, ", \t")) != NULL) {
151 size_t wordlen;
152
153 pr_signals_handle();
154
155 wordlen = strlen(word);
156 if (wordlen == 0) {
157 continue;
158 }
159
160 /* Remove any trailing comma */
161 if (word[wordlen-1] == ',') {
162 word[wordlen-1] = '\0';
163 wordlen--;
164 }
165
166 *((char **) push_array(clients)) = word;
167
168 /* Skip redundant whitespaces */
169 while (*dup_opts == ' ' ||
170 *dup_opts == '\t') {
171 pr_signals_handle();
172 dup_opts++;
173 }
174 }
175
176 } else {
177 *((char **) push_array(clients)) = pstrdup(redistab->tab_pool, val);
178 }
179 }
180
181 destroy_pool(tmp_pool);
182 return clients;
183 }
184
redistab_fetch_daemons_cb(wrap2_table_t * redistab,const char * name)185 static array_header *redistab_fetch_daemons_cb(wrap2_table_t *redistab,
186 const char *name) {
187 array_header *daemons_list;
188
189 /* Simply return the service name we're given. */
190 daemons_list = make_array(redistab->tab_pool, 1, sizeof(char *));
191 *((char **) push_array(daemons_list)) = pstrdup(redistab->tab_pool, name);
192
193 return daemons_list;
194 }
195
redistab_fetch_options_cb(wrap2_table_t * redistab,const char * name)196 static array_header *redistab_fetch_options_cb(wrap2_table_t *redistab,
197 const char *name) {
198 register unsigned int i;
199 pool *tmp_pool = NULL;
200 pr_redis_t *redis;
201 char *key = NULL, **vals = NULL;
202 array_header *items = NULL, *itemszs = NULL, *options = NULL;
203 int res, xerrno = 0, use_list = TRUE;
204
205 /* Allocate a temporary pool for the duration of this read. */
206 tmp_pool = make_sub_pool(redistab->tab_pool);
207
208 key = ((char **) redistab->tab_data)[WRAP2_REDIS_OPTION_KEY_IDX];
209
210 /* The options key is not necessary. Skip if not present. */
211 if (key == NULL) {
212 destroy_pool(tmp_pool);
213 return NULL;
214 }
215
216 if (strncasecmp(key, "list:", 5) == 0) {
217 key += 5;
218
219 } else if (strncasecmp(key, "set:", 4) == 0) {
220 use_list = FALSE;
221 key += 4;
222 }
223
224 key = get_named_key(tmp_pool, key, name);
225 redis = redistab->tab_handle;
226
227 if (use_list == TRUE) {
228 res = pr_redis_list_getall(tmp_pool, redis, &wrap2_redis_module, key,
229 &items, &itemszs);
230 xerrno = errno;
231
232 } else {
233 res = pr_redis_set_getall(tmp_pool, redis, &wrap2_redis_module, key,
234 &items, &itemszs);
235 xerrno = errno;
236 }
237
238 /* Check the results. */
239 if (res < 0) {
240 if (use_list == TRUE) {
241 wrap2_log("error obtaining options from Redis using list '%s': %s",
242 key, strerror(xerrno));
243
244 } else {
245 wrap2_log("error obtaining options from Redis using set '%s': %s",
246 key, strerror(xerrno));
247 }
248
249 destroy_pool(tmp_pool);
250 errno = xerrno;
251 return NULL;
252 }
253
254 if (items->nelts < 1) {
255 if (use_list == TRUE) {
256 wrap2_log("no options found in Redis using list '%s'", key);
257
258 } else {
259 wrap2_log("no options found in Redis using set '%s'", key);
260 }
261
262 destroy_pool(tmp_pool);
263 errno = ENOENT;
264 return NULL;
265 }
266
267 options = make_array(redistab->tab_pool, items->nelts, sizeof(char *));
268
269 vals = (char **) items->elts;
270
271 for (i = 0; i < items->nelts; i++) {
272 char *val;
273
274 if (vals[i] == NULL) {
275 continue;
276 }
277
278 /* Values in Redis are NOT NUL-terminated. */
279 val = pstrndup(tmp_pool, vals[i], ((size_t *) itemszs->elts)[i]);
280
281 *((char **) push_array(options)) = pstrdup(redistab->tab_pool, val);
282 }
283
284 destroy_pool(tmp_pool);
285 return options;
286 }
287
redistab_open_cb(pool * parent_pool,const char * srcinfo)288 static wrap2_table_t *redistab_open_cb(pool *parent_pool, const char *srcinfo) {
289 wrap2_table_t *tab = NULL;
290 pool *tab_pool = make_sub_pool(parent_pool),
291 *tmp_pool = make_sub_pool(parent_pool);
292 char *start = NULL, *finish = NULL, *info;
293 char *client_key = NULL, *option_key = NULL;
294 pr_redis_t *redis;
295
296 tab = (wrap2_table_t *) pcalloc(tab_pool, sizeof(wrap2_table_t));
297 tab->tab_pool = tab_pool;
298
299 /* The srcinfo string for this case should look like:
300 * "/list|set:<client-key>[/list|set:<options-key>]"
301 */
302
303 info = pstrdup(tmp_pool, srcinfo);
304 start = strchr(info, '/');
305 if (start == NULL) {
306 wrap2_log("error: badly formatted source info '%s'", srcinfo);
307 destroy_pool(tab_pool);
308 destroy_pool(tmp_pool);
309 errno = EINVAL;
310 return NULL;
311 }
312
313 /* Find the next slash. */
314 finish = strchr(++start, '/');
315 if (finish != NULL) {
316 *finish = '\0';
317 }
318
319 client_key = pstrdup(tab->tab_pool, start);
320
321 /* Handle the options list, if present. */
322 if (finish != NULL) {
323 option_key = pstrdup(tab->tab_pool, ++finish);
324 }
325
326 if (strncasecmp(client_key, "list:", 5) != 0 &&
327 strncasecmp(client_key, "set:", 4) != 0) {
328 wrap2_log("error: client key '%s' lacks required 'list:' or 'set:' prefix",
329 client_key);
330 destroy_pool(tab_pool);
331 destroy_pool(tmp_pool);
332 errno = EINVAL;
333 return NULL;
334 }
335
336 if (option_key != NULL) {
337 if (strncasecmp(option_key, "list:", 5) != 0 &&
338 strncasecmp(option_key, "set:", 4) != 0) {
339 wrap2_log("error: option key '%s' lacks required 'list:' or 'set:' "
340 "prefix", option_key);
341 destroy_pool(tab_pool);
342 destroy_pool(tmp_pool);
343 errno = EINVAL;
344 return NULL;
345 }
346 }
347
348 redis = pr_redis_conn_new(tab->tab_pool, &wrap2_redis_module, 0);
349 if (redis == NULL) {
350 int xerrno = errno;
351
352 wrap2_log("error: unable to open Redis connection: %s", strerror(xerrno));
353 destroy_pool(tab_pool);
354 destroy_pool(tmp_pool);
355 errno = xerrno;
356 return NULL;
357 }
358
359 tab->tab_handle = redis;
360 tab->tab_name = pstrcat(tab->tab_pool, "Redis(", info, ")", NULL);
361
362 tab->tab_data = pcalloc(tab->tab_pool, WRAP2_REDIS_NKEYS * sizeof(char *));
363 ((char **) tab->tab_data)[WRAP2_REDIS_CLIENT_KEY_IDX] =
364 pstrdup(tab->tab_pool, client_key);
365
366 ((char **) tab->tab_data)[WRAP2_REDIS_OPTION_KEY_IDX] =
367 pstrdup(tab->tab_pool, option_key);
368
369 /* Set the necessary callbacks. */
370 tab->tab_close = redistab_close_cb;
371 tab->tab_fetch_clients = redistab_fetch_clients_cb;
372 tab->tab_fetch_daemons = redistab_fetch_daemons_cb;
373 tab->tab_fetch_options = redistab_fetch_options_cb;
374
375 destroy_pool(tmp_pool);
376 return tab;
377 }
378
379 /* Event handlers
380 */
381
382 #if defined(PR_SHARED_MODULE)
redistab_mod_unload_ev(const void * event_data,void * user_data)383 static void redistab_mod_unload_ev(const void *event_data, void *user_data) {
384 if (strcmp("mod_wrap2_redis.c", (const char *) event_data) == 0) {
385 pr_event_unregister(&wrap2_redis_module, NULL, NULL);
386 wrap2_unregister("redis");
387 }
388 }
389 #endif /* PR_SHARED_MODULE */
390
391 /* Initialization routines
392 */
393
redistab_init(void)394 static int redistab_init(void) {
395
396 /* Initialize the wrap source objects for type "redis". */
397 wrap2_register("redis", redistab_open_cb);
398
399 #if defined(PR_SHARED_MODULE)
400 pr_event_register(&wrap2_redis_module, "core.module-unload",
401 redistab_mod_unload_ev, NULL);
402 #endif /* PR_SHARED_MODULE */
403
404 return 0;
405 }
406
redistab_sess_init(void)407 static int redistab_sess_init(void) {
408 config_rec *c;
409 int engine;
410
411 c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
412 if (c == NULL) {
413 return 0;
414 }
415
416 engine = *((int *) c->argv[0]);
417 if (engine == FALSE) {
418 return 0;
419 }
420
421 /* Note: These lookups duplicate what mod_redis does. But we do it here
422 * due to module load ordering; we want to make sure that Redis-based
423 * ACLs work properly with minimal fuss with regard to the module load
424 * order.
425 */
426
427 c = find_config(main_server->conf, CONF_PARAM, "RedisSentinel", FALSE);
428 if (c != NULL) {
429 array_header *sentinels;
430 const char *name;
431
432 sentinels = c->argv[0];
433 name = c->argv[1];
434
435 (void) redis_set_sentinels(sentinels, name);
436 }
437
438 c = find_config(main_server->conf, CONF_PARAM, "RedisServer", FALSE);
439 if (c != NULL) {
440 const char *server, *password, *db_idx;
441 int port;
442
443 server = c->argv[0];
444 port = *((int *) c->argv[1]);
445 password = c->argv[2];
446 db_idx = c->argv[3];
447
448 (void) redis_set_server(server, port, 0UL, password, db_idx);
449 }
450
451 c = find_config(main_server->conf, CONF_PARAM, "RedisTimeouts", FALSE);
452 if (c) {
453 unsigned long connect_millis, io_millis;
454
455 connect_millis = *((unsigned long *) c->argv[0]);
456 io_millis = *((unsigned long *) c->argv[1]);
457
458 (void) redis_set_timeouts(connect_millis, io_millis);
459 }
460
461 return 0;
462 }
463
464 /* Module API tables
465 */
466
467 module wrap2_redis_module = {
468 NULL, NULL,
469
470 /* Module API version 2.0 */
471 0x20,
472
473 /* Module name */
474 "wrap2_redis",
475
476 /* Module configuration handler table */
477 NULL,
478
479 /* Module command handler table */
480 NULL,
481
482 /* Module authentication handler table */
483 NULL,
484
485 /* Module initialization function */
486 redistab_init,
487
488 /* Session initialization function */
489 redistab_sess_init,
490
491 /* Module version */
492 MOD_WRAP2_REDIS_VERSION
493 };
494