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