1 /*
2  * ProFTPD: mod_redis -- a module for managing Redis data
3  * Copyright (c) 2017-2020 The ProFTPD Project
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18  *
19  * As a special exemption, TJ Saunders and other respective copyright holders
20  * give permission to link this program with OpenSSL, and distribute the
21  * resulting executable, without including the source code for OpenSSL in the
22  * source distribution.
23  *
24  * -----DO NOT EDIT BELOW THIS LINE-----
25  * $Libraries: -lhiredis$
26  */
27 
28 #include "conf.h"
29 #include "privs.h"
30 #include "logfmt.h"
31 #include "json.h"
32 #include "jot.h"
33 
34 #define MOD_REDIS_VERSION		"mod_redis/0.2.2"
35 
36 #if PROFTPD_VERSION_NUMBER < 0x0001030605
37 # error "ProFTPD 1.3.6rc5 or later required"
38 #endif
39 
40 #include <hiredis/hiredis.h>
41 
42 extern xaset_t *server_list;
43 
44 module redis_module;
45 
46 #define REDIS_SERVER_DEFAULT_PORT		6379
47 #define REDIS_SENTINEL_DEFAULT_PORT		26379
48 
49 static int redis_engine = FALSE;
50 static int redis_logfd = -1;
51 static unsigned long redis_opts = 0UL;
52 static pool *redis_pool = NULL;
53 
54 static int redis_sess_init(void);
55 
56 static pr_table_t *jot_logfmt2json = NULL;
57 static const char *trace_channel = "redis";
58 
59 /* TODO: Refactor this into a pr_jot_resolved_t structure, with shared/common
60  * resolver callbacks, similar to the shared parser callbacks.
61  */
62 
63 struct redis_buffer {
64   char *ptr, *buf;
65   size_t bufsz, buflen;
66 };
67 
redis_buffer_append_text(struct redis_buffer * log,const char * text,size_t text_len)68 static void redis_buffer_append_text(struct redis_buffer *log, const char *text,
69     size_t text_len) {
70   if (text == NULL ||
71       text_len == 0) {
72     return;
73   }
74 
75   if (text_len > log->buflen) {
76     text_len = log->buflen;
77   }
78 
79   pr_trace_msg(trace_channel, 19, "appending text '%.*s' (%lu) to buffer",
80     (int) text_len, text, (unsigned long) text_len);
81   memcpy(log->buf, text, text_len);
82   log->buf += text_len;
83   log->buflen -= text_len;
84 }
85 
resolve_on_meta(pool * p,pr_jot_ctx_t * jot_ctx,unsigned char logfmt_id,const char * jot_hint,const void * val)86 static int resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
87     unsigned char logfmt_id, const char *jot_hint, const void *val) {
88   struct redis_buffer *log;
89 
90   log = jot_ctx->log;
91   if (log->buflen > 0) {
92     const char *text = NULL;
93     size_t text_len = 0;
94     char buf[1024];
95 
96     switch (logfmt_id) {
97       case LOGFMT_META_MICROSECS: {
98         unsigned long num;
99 
100         num = *((double *) val);
101         text_len = pr_snprintf(buf, sizeof(buf)-1, "%06lu", num);
102         buf[text_len] = '\0';
103         text = buf;
104         break;
105       }
106 
107       case LOGFMT_META_MILLISECS: {
108         unsigned long num;
109 
110         num = *((double *) val);
111         text_len = pr_snprintf(buf, sizeof(buf)-1, "%03lu", num);
112         buf[text_len] = '\0';
113         text = buf;
114         break;
115       }
116 
117       case LOGFMT_META_LOCAL_PORT:
118       case LOGFMT_META_REMOTE_PORT:
119       case LOGFMT_META_RESPONSE_CODE:
120       case LOGFMT_META_XFER_PORT: {
121         int num;
122 
123         num = *((double *) val);
124         text_len = pr_snprintf(buf, sizeof(buf)-1, "%d", num);
125         buf[text_len] = '\0';
126         text = buf;
127         break;
128       }
129 
130       case LOGFMT_META_UID: {
131         uid_t uid;
132 
133         uid = *((double *) val);
134         text = pr_uid2str(p, uid);
135         break;
136       }
137 
138       case LOGFMT_META_GID: {
139         gid_t gid;
140 
141         gid = *((double *) val);
142         text = pr_gid2str(p, gid);
143         break;
144       }
145 
146       case LOGFMT_META_BYTES_SENT:
147       case LOGFMT_META_FILE_OFFSET:
148       case LOGFMT_META_FILE_SIZE:
149       case LOGFMT_META_RAW_BYTES_IN:
150       case LOGFMT_META_RAW_BYTES_OUT:
151       case LOGFMT_META_RESPONSE_MS:
152       case LOGFMT_META_XFER_MS: {
153         off_t num;
154 
155         num = *((double *) val);
156         text_len = pr_snprintf(buf, sizeof(buf)-1, "%" PR_LU, (pr_off_t) num);
157         buf[text_len] = '\0';
158         text = buf;
159         break;
160       }
161 
162       case LOGFMT_META_EPOCH:
163       case LOGFMT_META_PID: {
164         unsigned long num;
165 
166         num = *((double *) val);
167         text_len = pr_snprintf(buf, sizeof(buf)-1, "%lu", num);
168         buf[text_len] = '\0';
169         text = buf;
170         break;
171       }
172 
173       case LOGFMT_META_FILE_MODIFIED: {
174         int truth;
175 
176         truth = *((int *) val);
177         text = truth ? "true" : "false";
178         break;
179       }
180 
181       case LOGFMT_META_SECONDS: {
182         float num;
183 
184         num = *((double *) val);
185         text_len = pr_snprintf(buf, sizeof(buf)-1, "%0.3f", num);
186         buf[text_len] = '\0';
187         text = buf;
188         break;
189       }
190 
191       case LOGFMT_META_ANON_PASS:
192       case LOGFMT_META_BASENAME:
193       case LOGFMT_META_CLASS:
194       case LOGFMT_META_CMD_PARAMS:
195       case LOGFMT_META_COMMAND:
196       case LOGFMT_META_DIR_NAME:
197       case LOGFMT_META_DIR_PATH:
198       case LOGFMT_META_ENV_VAR:
199       case LOGFMT_META_EOS_REASON:
200       case LOGFMT_META_FILENAME:
201       case LOGFMT_META_GROUP:
202       case LOGFMT_META_IDENT_USER:
203       case LOGFMT_META_ISO8601:
204       case LOGFMT_META_LOCAL_FQDN:
205       case LOGFMT_META_LOCAL_IP:
206       case LOGFMT_META_LOCAL_NAME:
207       case LOGFMT_META_METHOD:
208       case LOGFMT_META_NOTE_VAR:
209       case LOGFMT_META_ORIGINAL_USER:
210       case LOGFMT_META_PROTOCOL:
211       case LOGFMT_META_REMOTE_HOST:
212       case LOGFMT_META_REMOTE_IP:
213       case LOGFMT_META_RENAME_FROM:
214       case LOGFMT_META_RESPONSE_STR:
215       case LOGFMT_META_TIME:
216       case LOGFMT_META_USER:
217       case LOGFMT_META_VERSION:
218       case LOGFMT_META_VHOST_IP:
219       case LOGFMT_META_XFER_FAILURE:
220       case LOGFMT_META_XFER_PATH:
221       case LOGFMT_META_XFER_STATUS:
222       case LOGFMT_META_XFER_TYPE:
223       default:
224         text = val;
225     }
226 
227     if (text != NULL &&
228         text_len == 0) {
229       text_len = strlen(text);
230     }
231 
232     redis_buffer_append_text(log, text, text_len);
233   }
234 
235   return 0;
236 }
237 
resolve_on_other(pool * p,pr_jot_ctx_t * jot_ctx,unsigned char * text,size_t text_len)238 static int resolve_on_other(pool *p, pr_jot_ctx_t *jot_ctx, unsigned char *text,
239     size_t text_len) {
240   struct redis_buffer *log;
241 
242   log = jot_ctx->log;
243   redis_buffer_append_text(log, (const char *) text, text_len);
244   return 0;
245 }
246 
log_event(pr_redis_t * redis,config_rec * c,cmd_rec * cmd)247 static void log_event(pr_redis_t *redis, config_rec *c, cmd_rec *cmd) {
248   pool *tmp_pool;
249   int res;
250   pr_jot_ctx_t *jot_ctx;
251   pr_jot_filters_t *jot_filters;
252   pr_json_object_t *json;
253   const char *fmt_name = NULL;
254   char *payload = NULL;
255   size_t payload_len = 0;
256   unsigned char *log_fmt, *key_fmt;
257 
258   jot_filters = c->argv[0];
259   fmt_name = c->argv[1];
260   log_fmt = c->argv[2];
261   key_fmt = c->argv[3];
262 
263   if (jot_filters == NULL ||
264       fmt_name == NULL ||
265       log_fmt == NULL) {
266     return;
267   }
268 
269   tmp_pool = make_sub_pool(cmd->tmp_pool);
270   jot_ctx = pcalloc(tmp_pool, sizeof(pr_jot_ctx_t));
271   json = pr_json_object_alloc(tmp_pool);
272   jot_ctx->log = json;
273   jot_ctx->user_data = jot_logfmt2json;
274 
275   res = pr_jot_resolve_logfmt(tmp_pool, cmd, jot_filters, log_fmt, jot_ctx,
276     pr_jot_on_json, NULL, NULL);
277   if (res == 0) {
278     payload = pr_json_object_to_text(tmp_pool, json, "");
279     payload_len = strlen(payload);
280     pr_trace_msg(trace_channel, 8, "generated JSON payload for %s: %.*s",
281       (char *) cmd->argv[0], (int) payload_len, payload);
282 
283   } else {
284     /* EPERM indicates that the message was filtered. */
285     if (errno != EPERM) {
286       (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
287         "error generating JSON formatted log message: %s", strerror(errno));
288     }
289 
290     payload = NULL;
291     payload_len = 0;
292   }
293 
294   pr_json_object_free(json);
295 
296   if (payload_len > 0) {
297     const char *key;
298 
299     if (key_fmt != NULL) {
300       struct redis_buffer *rb;
301       char key_buf[1024];
302 
303       rb = pcalloc(tmp_pool, sizeof(struct redis_buffer));
304       rb->bufsz = rb->buflen = sizeof(key_buf)-1;
305       rb->ptr = rb->buf = key_buf;
306 
307       jot_ctx->log = rb;
308 
309       res = pr_jot_resolve_logfmt(tmp_pool, cmd, NULL, key_fmt, jot_ctx,
310         resolve_on_meta, NULL, resolve_on_other);
311       if (res == 0) {
312         size_t key_buflen;
313 
314         key_buflen = rb->bufsz - rb->buflen;
315         key = pstrndup(tmp_pool, key_buf, key_buflen);
316 
317       } else {
318         (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
319           "error resolving Redis key format: %s", strerror(errno));
320         key = fmt_name;
321       }
322 
323     } else {
324       key = fmt_name;
325     }
326 
327     res = pr_redis_list_append(redis, &redis_module, key, payload,
328       payload_len);
329     if (res < 0) {
330       (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
331         "error appending log message to '%s': %s", key, strerror(errno));
332 
333     } else {
334       pr_trace_msg(trace_channel, 17, "appended log message to '%s'", key);
335     }
336   }
337 
338   destroy_pool(tmp_pool);
339 }
340 
log_events(cmd_rec * cmd)341 static void log_events(cmd_rec *cmd) {
342   pr_redis_t *redis;
343   config_rec *c;
344 
345   redis = pr_redis_conn_get(session.pool, redis_opts);
346   if (redis == NULL) {
347     (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
348       "error connecting to Redis: %s", strerror(errno));
349     return;
350   }
351 
352   c = find_config(CURRENT_CONF, CONF_PARAM, "RedisLogOnCommand", FALSE);
353   while (c != NULL) {
354     pr_signals_handle();
355 
356     log_event(redis, c, cmd);
357     c = find_config_next(c, c->next, CONF_PARAM, "RedisLogOnCommand", FALSE);
358   }
359 
360   c = find_config(CURRENT_CONF, CONF_PARAM, "RedisLogOnEvent", FALSE);
361   while (c != NULL) {
362     pr_signals_handle();
363 
364     log_event(redis, c, cmd);
365     c = find_config_next(c, c->next, CONF_PARAM, "RedisLogOnEvent", FALSE);
366   }
367 }
368 
369 /* Configuration handlers
370  */
371 
372 /* usage: RedisEngine on|off */
set_redisengine(cmd_rec * cmd)373 MODRET set_redisengine(cmd_rec *cmd) {
374   int engine = -1;
375   config_rec *c = NULL;
376 
377   CHECK_ARGS(cmd, 1);
378   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
379 
380   engine = get_boolean(cmd, 1);
381   if (engine == -1) {
382     CONF_ERROR(cmd, "expected Boolean parameter");
383   }
384 
385   c = add_config_param(cmd->argv[0], 1, NULL);
386   c->argv[0] = pcalloc(c->pool, sizeof(int));
387   *((int *) c->argv[0]) = engine;
388 
389   return PR_HANDLED(cmd);
390 }
391 
392 /* usage: RedisLog path|"none" */
set_redislog(cmd_rec * cmd)393 MODRET set_redislog(cmd_rec *cmd) {
394   CHECK_ARGS(cmd, 1);
395   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
396 
397   if (strcasecmp(cmd->argv[1], "none") != 0 &&
398       pr_fs_valid_path(cmd->argv[1]) < 0) {
399     CONF_ERROR(cmd, "must be an absolute path");
400   }
401 
402   add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
403   return PR_HANDLED(cmd);
404 }
405 
406 /* usage: RedisLogOnCommand "none"|commands log-fmt [key] */
set_redislogoncommand(cmd_rec * cmd)407 MODRET set_redislogoncommand(cmd_rec *cmd) {
408   config_rec *c, *logfmt_config;
409   const char *fmt_name, *rules;
410   unsigned char *log_fmt = NULL, *key_fmt = NULL;
411   pr_jot_filters_t *jot_filters;
412 
413   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL|CONF_ANON|CONF_DIR);
414 
415   if (cmd->argc < 3 ||
416       cmd->argc > 4) {
417 
418     if (cmd->argc == 2 &&
419         strcasecmp(cmd->argv[1], "none") == 0) {
420        c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
421        c->flags |= CF_MERGEDOWN;
422        return PR_HANDLED(cmd);
423     }
424 
425     CONF_ERROR(cmd, "wrong number of parameters");
426   }
427 
428   c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
429 
430   rules = cmd->argv[1];
431   jot_filters = pr_jot_filters_create(c->pool, rules,
432     PR_JOT_FILTER_TYPE_COMMANDS, 0);
433   if (jot_filters == NULL) {
434     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use commands '", rules,
435       "': ", strerror(errno), NULL));
436   }
437 
438   fmt_name = cmd->argv[2];
439 
440   /* Make sure that the given LogFormat name is known. */
441   logfmt_config = find_config(cmd->server->conf, CONF_PARAM, "LogFormat",
442     FALSE);
443   while (logfmt_config != NULL) {
444     pr_signals_handle();
445 
446     if (strcmp(fmt_name, logfmt_config->argv[0]) == 0) {
447       log_fmt = logfmt_config->argv[1];
448       break;
449     }
450 
451     logfmt_config = find_config_next(logfmt_config, logfmt_config->next,
452       CONF_PARAM, "LogFormat", FALSE);
453   }
454 
455   if (log_fmt == NULL) {
456     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "no LogFormat '", fmt_name,
457       "' configured", NULL));
458   }
459 
460   if (cmd->argc == 4) {
461     int res;
462     pool *tmp_pool;
463     pr_jot_ctx_t *jot_ctx;
464     pr_jot_parsed_t *jot_parsed;
465     char *key_text;
466     unsigned char key_fmtbuf[1024];
467     size_t key_fmtlen;
468 
469     tmp_pool = make_sub_pool(cmd->tmp_pool);
470     jot_ctx = pcalloc(tmp_pool, sizeof(pr_jot_ctx_t));
471     jot_parsed = pcalloc(tmp_pool, sizeof(pr_jot_parsed_t));
472     jot_parsed->bufsz = jot_parsed->buflen = sizeof(key_fmtbuf)-1;
473     jot_parsed->ptr = jot_parsed->buf = key_fmtbuf;
474 
475     jot_ctx->log = jot_parsed;
476 
477     key_text = cmd->argv[3];
478     res = pr_jot_parse_logfmt(tmp_pool, key_text, jot_ctx,
479       pr_jot_parse_on_meta, pr_jot_parse_on_unknown, pr_jot_parse_on_other, 0);
480     if (res < 0) {
481       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing Redis key '",
482         key_text, "': ", strerror(errno), NULL));
483     }
484 
485     destroy_pool(tmp_pool);
486 
487     key_fmtlen = jot_parsed->bufsz - jot_parsed->buflen;
488     key_fmtbuf[key_fmtlen] = '\0';
489     key_fmt = (unsigned char *) pstrndup(c->pool, (char *) key_fmtbuf,
490       key_fmtlen);
491   }
492 
493   c->argv[0] = jot_filters;
494   c->argv[1] = pstrdup(c->pool, fmt_name);
495   c->argv[2] = log_fmt;
496   c->argv[3] = key_fmt;
497 
498   c->flags |= CF_MERGEDOWN_MULTI;
499   return PR_HANDLED(cmd);
500 }
501 
502 /* usage: RedisLogOnEvent "none"|events log-fmt [key] */
set_redislogonevent(cmd_rec * cmd)503 MODRET set_redislogonevent(cmd_rec *cmd) {
504   config_rec *c, *logfmt_config;
505   const char *fmt_name, *rules;
506   unsigned char *log_fmt = NULL, *key_fmt = NULL;
507   pr_jot_filters_t *jot_filters;
508 
509   CHECK_CONF(cmd, CONF_ROOT|CONF_GLOBAL|CONF_VIRTUAL|CONF_ANON|CONF_DIR);
510 
511   if (cmd->argc < 3 ||
512       cmd->argc > 4) {
513 
514     if (cmd->argc == 2 &&
515         strcasecmp(cmd->argv[1], "none") == 0) {
516        c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
517        c->flags |= CF_MERGEDOWN;
518        return PR_HANDLED(cmd);
519     }
520 
521     CONF_ERROR(cmd, "wrong number of parameters");
522   }
523 
524   c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
525 
526   rules = cmd->argv[1];
527   jot_filters = pr_jot_filters_create(c->pool, rules,
528     PR_JOT_FILTER_TYPE_COMMANDS_WITH_CLASSES,
529     PR_JOT_FILTER_FL_ALL_INCL_ALL);
530   if (jot_filters == NULL) {
531     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "unable to use events '", rules,
532       "': ", strerror(errno), NULL));
533   }
534 
535   fmt_name = cmd->argv[2];
536 
537   /* Make sure that the given LogFormat name is known. */
538   logfmt_config = find_config(cmd->server->conf, CONF_PARAM, "LogFormat",
539     FALSE);
540   while (logfmt_config != NULL) {
541     pr_signals_handle();
542 
543     if (strcmp(fmt_name, logfmt_config->argv[0]) == 0) {
544       log_fmt = logfmt_config->argv[1];
545       break;
546     }
547 
548     logfmt_config = find_config_next(logfmt_config, logfmt_config->next,
549       CONF_PARAM, "LogFormat", FALSE);
550   }
551 
552   if (log_fmt == NULL) {
553     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "no LogFormat '", fmt_name,
554       "' configured", NULL));
555   }
556 
557   if (cmd->argc == 4) {
558     int res;
559     pool *tmp_pool;
560     pr_jot_ctx_t *jot_ctx;
561     pr_jot_parsed_t *jot_parsed;
562     char *key_text;
563     unsigned char key_fmtbuf[1024];
564     size_t key_fmtlen;
565 
566     tmp_pool = make_sub_pool(cmd->tmp_pool);
567     jot_ctx = pcalloc(tmp_pool, sizeof(pr_jot_ctx_t));
568     jot_parsed = pcalloc(tmp_pool, sizeof(pr_jot_parsed_t));
569     jot_parsed->bufsz = jot_parsed->buflen = sizeof(key_fmtbuf)-1;
570     jot_parsed->ptr = jot_parsed->buf = key_fmtbuf;
571 
572     jot_ctx->log = jot_parsed;
573 
574     key_text = cmd->argv[3];
575     res = pr_jot_parse_logfmt(tmp_pool, key_text, jot_ctx,
576       pr_jot_parse_on_meta, pr_jot_parse_on_unknown, pr_jot_parse_on_other, 0);
577     if (res < 0) {
578       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error parsing Redis key '",
579         key_text, "': ", strerror(errno), NULL));
580     }
581 
582     destroy_pool(tmp_pool);
583 
584     key_fmtlen = jot_parsed->bufsz - jot_parsed->buflen;
585     key_fmtbuf[key_fmtlen] = '\0';
586     key_fmt = (unsigned char *) pstrndup(c->pool, (char *) key_fmtbuf,
587       key_fmtlen);
588   }
589 
590   c->argv[0] = jot_filters;
591   c->argv[1] = pstrdup(c->pool, fmt_name);
592   c->argv[2] = log_fmt;
593   c->argv[3] = key_fmt;
594 
595   c->flags |= CF_MERGEDOWN_MULTI;
596   return PR_HANDLED(cmd);
597 }
598 
599 /* usage: RedisOptions opt1 opt2 ... */
set_redisoptions(cmd_rec * cmd)600 MODRET set_redisoptions(cmd_rec *cmd) {
601   config_rec *c = NULL;
602   register unsigned int i = 0;
603   unsigned long opts = 0UL;
604 
605   if (cmd->argc-1 == 0) {
606     CONF_ERROR(cmd, "wrong number of parameters");
607   }
608 
609   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
610 
611   c = add_config_param(cmd->argv[0], 1, NULL);
612 
613   for (i = 1; i < cmd->argc; i++) {
614     if (strcmp(cmd->argv[i], "NoReconnect") == 0) {
615       opts |= PR_REDIS_CONN_FL_NO_RECONNECT;
616 
617     } else {
618       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown RedisOption '",
619         cmd->argv[i], "'", NULL));
620     }
621   }
622 
623   c->argv[0] = pcalloc(c->pool, sizeof(unsigned long));
624   *((unsigned long *) c->argv[0]) = opts;
625 
626   return PR_HANDLED(cmd);
627 }
628 
629 /* usage: RedisSentinel host[:port] ... [master name] */
set_redissentinel(cmd_rec * cmd)630 MODRET set_redissentinel(cmd_rec *cmd) {
631   register unsigned int i;
632   config_rec *c;
633   array_header *sentinels;
634   char *master_name = NULL;
635 
636   CHECK_ARGS(cmd, 1);
637   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
638 
639   if (cmd->argc >= 4) {
640     /* Has the name of a master been explicitly provided? */
641     if (strcasecmp(cmd->argv[cmd->argc-2], "master") == 0) {
642       master_name = cmd->argv[cmd->argc-1];
643 
644       cmd->argc -= 2;
645     }
646   }
647 
648   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
649   sentinels = make_array(c->pool, 0, sizeof(pr_netaddr_t *));
650 
651   for (i = 1; i < cmd->argc; i++) {
652     char *sentinel, *ptr;
653     size_t sentinel_len;
654     int port = REDIS_SENTINEL_DEFAULT_PORT;
655     pr_netaddr_t *sentinel_addr;
656 
657     sentinel = pstrdup(cmd->tmp_pool, cmd->argv[i]);
658     sentinel_len = strlen(sentinel);
659 
660     ptr = strrchr(sentinel, ':');
661     if (ptr != NULL) {
662       /* We also need to check for IPv6 addresses, e.g. "[::1]" or
663        * "[::1]:26379", before assuming that the text following our discovered
664        * ':' is indeed a port number.
665        */
666       if (*sentinel == '[') {
667         if (*(ptr-1) == ']') {
668           /* We have an IPv6 address with an explicit port number. */
669           sentinel = pstrndup(cmd->tmp_pool, sentinel + 1,
670             (ptr - 1) - (sentinel + 1));
671           *ptr = '\0';
672           port = atoi(ptr + 1);
673 
674         } else if (sentinel[sentinel_len-1] == ']') {
675           /* We have an IPv6 address without an explicit port number. */
676           sentinel = pstrndup(cmd->tmp_pool, sentinel + 1, sentinel_len - 2);
677           port = REDIS_SENTINEL_DEFAULT_PORT;
678         }
679 
680       } else {
681         *ptr = '\0';
682         port = atoi(ptr + 1);
683       }
684     }
685 
686     sentinel_addr = (pr_netaddr_t *) pr_netaddr_get_addr(c->pool, sentinel,
687       NULL);
688     if (sentinel_addr != NULL) {
689       pr_netaddr_set_port2(sentinel_addr, port);
690       *((pr_netaddr_t **) push_array(sentinels)) = sentinel_addr;
691 
692     } else {
693       pr_log_debug(DEBUG0, "%s: unable to resolve '%s' (%s), ignoring",
694         (char *) cmd->argv[0], sentinel, strerror(errno));
695     }
696   }
697 
698   c->argv[0] = sentinels;
699   c->argv[1] = pstrdup(c->pool, master_name);
700 
701   return PR_HANDLED(cmd);
702 }
703 
704 /* usage: RedisServer host[:port] [password] [db-index] */
set_redisserver(cmd_rec * cmd)705 MODRET set_redisserver(cmd_rec *cmd) {
706   config_rec *c;
707   char *server, *password = NULL, *db_idx = NULL, *ptr;
708   size_t server_len;
709   int ctx, port = REDIS_SERVER_DEFAULT_PORT;
710 
711   CHECK_ARGS(cmd, 1);
712   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
713 
714   server = pstrdup(cmd->tmp_pool, cmd->argv[1]);
715   server_len = strlen(server);
716 
717   ptr = strrchr(server, ':');
718   if (ptr != NULL) {
719     /* We also need to check for IPv6 addresses, e.g. "[::1]" or "[::1]:6379",
720      * before assuming that the text following our discovered ':' is indeed
721      * a port number.
722      */
723 
724     if (*server == '[') {
725       if (*(ptr-1) == ']') {
726         /* We have an IPv6 address with an explicit port number. */
727         server = pstrndup(cmd->tmp_pool, server + 1, (ptr - 1) - (server + 1));
728         *ptr = '\0';
729         port = atoi(ptr + 1);
730 
731       } else if (server[server_len-1] == ']') {
732         /* We have an IPv6 address without an explicit port number. */
733         server = pstrndup(cmd->tmp_pool, server + 1, server_len - 2);
734         port = REDIS_SERVER_DEFAULT_PORT;
735       }
736 
737     } else {
738       *ptr = '\0';
739       port = atoi(ptr + 1);
740     }
741   }
742 
743   if (cmd->argc == 3) {
744     password = cmd->argv[2];
745     if (strcmp(password, "") == 0) {
746       password = NULL;
747     }
748   }
749 
750   if (cmd->argc == 4) {
751     db_idx = cmd->argv[3];
752     if (strcmp(db_idx, "") == 0) {
753       db_idx = NULL;
754     }
755   }
756 
757   c = add_config_param(cmd->argv[0], 4, NULL, NULL, NULL, NULL);
758   c->argv[0] = pstrdup(c->pool, server);
759   c->argv[1] = palloc(c->pool, sizeof(int));
760   *((int *) c->argv[1]) = port;
761   c->argv[2] = pstrdup(c->pool, password);
762   c->argv[3] = pstrdup(c->pool, db_idx);
763 
764   ctx = (cmd->config && cmd->config->config_type != CONF_PARAM ?
765     cmd->config->config_type : cmd->server->config_type ?
766     cmd->server->config_type : CONF_ROOT);
767 
768   if (ctx == CONF_ROOT) {
769     /* If we're the "server config" context, set the server now.  This
770      * would let mod_redis talk to those servers for e.g. ftpdctl actions.
771      */
772     (void) redis_set_server(c->argv[0], port, 0UL, c->argv[2], c->argv[3]);
773   }
774 
775   return PR_HANDLED(cmd);
776 }
777 
778 /* usage: RedisTimeouts conn-timeout io-timeout */
set_redistimeouts(cmd_rec * cmd)779 MODRET set_redistimeouts(cmd_rec *cmd) {
780   config_rec *c;
781   unsigned long connect_millis, io_millis;
782   char *ptr = NULL;
783 
784   CHECK_ARGS(cmd, 2);
785   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
786 
787   connect_millis = strtoul(cmd->argv[1], &ptr, 10);
788   if (ptr && *ptr) {
789     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
790       "badly formatted connect timeout value: ", cmd->argv[1], NULL));
791   }
792 
793   ptr = NULL;
794   io_millis = strtoul(cmd->argv[2], &ptr, 10);
795   if (ptr && *ptr) {
796     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
797       "badly formatted IO timeout value: ", cmd->argv[2], NULL));
798   }
799 
800 #if 0
801   /* XXX If we're the "server config" context, set the timeouts now.
802    * This would let mod_redis talk to those servers for e.g. ftpdctl
803    * actions.
804    */
805   redis_set_timeouts(conn_timeout, io_timeout);
806 #endif
807 
808   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
809   c->argv[0] = palloc(c->pool, sizeof(unsigned long));
810   *((unsigned long *) c->argv[0]) = connect_millis;
811   c->argv[1] = palloc(c->pool, sizeof(unsigned long));
812   *((unsigned long *) c->argv[1]) = io_millis;
813 
814   return PR_HANDLED(cmd);
815 }
816 
817 /* Command handlers
818  */
819 
redis_log_any(cmd_rec * cmd)820 MODRET redis_log_any(cmd_rec *cmd) {
821   if (redis_engine == FALSE) {
822     return PR_DECLINED(cmd);
823   }
824 
825   log_events(cmd);
826   return PR_DECLINED(cmd);
827 }
828 
is_redis_log_config(config_rec * c,const char * name,size_t namelen,unsigned int config_id)829 static int is_redis_log_config(config_rec *c, const char *name, size_t namelen,
830     unsigned int config_id) {
831   int matched = FALSE;
832 
833   if (c->config_type == CONF_PARAM &&
834       c->argc == 4 &&
835       c->config_id == config_id &&
836       strncmp(name, c->name, namelen + 1) == 0) {
837     matched = TRUE;
838   }
839 
840   return matched;
841 }
842 
prune_redis_log_config(pool * p,xaset_t * set,const char * name,size_t namelen,unsigned int config_id)843 static int prune_redis_log_config(pool *p, xaset_t *set, const char *name,
844     size_t namelen, unsigned int config_id) {
845   config_rec *c;
846   int needs_pruning = FALSE, pruned = FALSE;
847 
848   /* Bottom up pruning; handle subsets first. */
849   for (c = (config_rec *) set->xas_list; c; c = c->next) {
850     pr_signals_handle();
851 
852     if (c->subset == NULL) {
853       continue;
854     }
855 
856     if (prune_redis_log_config(p, c->subset, name, namelen,
857         config_id) == TRUE) {
858       pruned = TRUE;
859     }
860   }
861 
862   /* Look for the named directive, specifically looking for any "none"
863    * configurations.
864    */
865   for (c = (config_rec *) set->xas_list; c; c = c->next) {
866     pr_signals_handle();
867 
868     if (is_redis_log_config(c, name, namelen, config_id) == TRUE) {
869       if (c->argv[0] == NULL ||
870           c->argv[1] == NULL ||
871           c->argv[2] == NULL) {
872         needs_pruning = TRUE;
873         break;
874       }
875     }
876   }
877 
878   if (needs_pruning) {
879     pr_config_remove(set, name, 0, FALSE);
880     pruned = TRUE;
881   }
882 
883   return pruned;
884 }
885 
redis_post_pass(cmd_rec * cmd)886 MODRET redis_post_pass(cmd_rec *cmd) {
887   const char *name;
888   int pruned = FALSE;
889 
890   if (redis_engine == FALSE) {
891     return PR_DECLINED(cmd);
892   }
893 
894   if (main_server->conf == NULL ||
895       main_server->conf->xas_list == NULL) {
896     return PR_DECLINED(cmd);
897   }
898 
899   /* Process the config tree for RedisLogOnCommand/Event "none" directives,
900    * so that they work as expected.
901    *
902    * Since RedisLogOnCommand/Event allow for multiple simultaneous occurrences
903    * (via CF_MERGEDOWN_MULTI), due to multiple possible filters/formats, we
904    * cannot simply assume that the CF_MERGEDOWN flag for the "none" case will
905    * do as we want.  Instead, we have manually prune any competing
906    * RedisLogOnCommand/Event directives, for a given directory level, whenever
907    * we encounter a "none" directive.
908    */
909 
910   name = "RedisLogOnCommand";
911   if (find_config(main_server->conf, CONF_PARAM, name, TRUE) != NULL) {
912     size_t namelen;
913     unsigned int config_id;
914 
915     namelen = strlen(name);
916     config_id = pr_config_get_id(name);
917     if (prune_redis_log_config(cmd->tmp_pool, main_server->conf, name, namelen,
918         config_id) == TRUE) {
919       pruned = TRUE;
920     }
921   }
922 
923   name = "RedisLogOnEvent";
924   if (find_config(main_server->conf, CONF_PARAM, name, TRUE) != NULL) {
925     size_t namelen;
926     unsigned int config_id;
927 
928     namelen = strlen(name);
929     config_id = pr_config_get_id(name);
930     if (prune_redis_log_config(cmd->tmp_pool, main_server->conf, name, namelen,
931         config_id) == TRUE) {
932       pruned = TRUE;
933     }
934   }
935 
936   if (pruned) {
937     pr_log_debug(DEBUG9, MOD_REDIS_VERSION
938       ": Pruned configuration for Redis logging");
939     pr_config_dump(NULL, main_server->conf, NULL);
940   }
941 
942   return PR_DECLINED(cmd);
943 }
944 
945 /* Event handlers
946  */
947 
redis_restart_ev(const void * event_data,void * user_data)948 static void redis_restart_ev(const void *event_data, void *user_data) {
949   destroy_pool(redis_pool);
950   redis_pool = make_sub_pool(permanent_pool);
951   pr_pool_tag(redis_pool, MOD_REDIS_VERSION);
952 
953   jot_logfmt2json = pr_jot_get_logfmt2json(redis_pool);
954 }
955 
redis_sess_reinit_ev(const void * event_data,void * user_data)956 static void redis_sess_reinit_ev(const void *event_data, void *user_data) {
957   int res;
958 
959   /* A HOST command changed the main_server pointer, reinitialize ourselves. */
960 
961   pr_event_unregister(&redis_module, "core.session-reinit",
962     redis_sess_reinit_ev);
963 
964   (void) close(redis_logfd);
965   redis_logfd = -1;
966 
967   /* XXX Restore other Redis settings? */
968   /* reset RedisTimeouts */
969 
970   res = redis_sess_init();
971   if (res < 0) {
972     pr_session_disconnect(&redis_module,
973       PR_SESS_DISCONNECT_SESSION_INIT_FAILED, NULL);
974   }
975 }
976 
redis_shutdown_ev(const void * event_data,void * user_data)977 static void redis_shutdown_ev(const void *event_data, void *user_data) {
978   destroy_pool(redis_pool);
979   jot_logfmt2json = NULL;
980 }
981 
982 /* Initialization functions
983  */
984 
redis_module_init(void)985 static int redis_module_init(void) {
986   redis_pool = make_sub_pool(permanent_pool);
987   pr_pool_tag(redis_pool, MOD_REDIS_VERSION);
988 
989   redis_init();
990   pr_event_register(&redis_module, "core.restart", redis_restart_ev, NULL);
991   pr_event_register(&redis_module, "core.shutdown", redis_shutdown_ev, NULL);
992 
993   pr_log_debug(DEBUG2, MOD_REDIS_VERSION ": using hiredis-%d.%d.%d",
994     HIREDIS_MAJOR, HIREDIS_MINOR, HIREDIS_PATCH);
995 
996   jot_logfmt2json = pr_jot_get_logfmt2json(redis_pool);
997   if (jot_logfmt2json == NULL) {
998     return -1;
999   }
1000 
1001   return 0;
1002 }
1003 
redis_sess_init(void)1004 static int redis_sess_init(void) {
1005   config_rec *c;
1006 
1007   pr_event_register(&redis_module, "core.session-reinit",
1008     redis_sess_reinit_ev, NULL);
1009 
1010   c = find_config(main_server->conf, CONF_PARAM, "RedisEngine", FALSE);
1011   if (c != NULL) {
1012     int engine;
1013 
1014     engine = *((int *) c->argv[0]);
1015     if (engine == FALSE) {
1016       return 0;
1017     }
1018 
1019     redis_engine = engine;
1020   }
1021 
1022   c = find_config(main_server->conf, CONF_PARAM, "RedisLog", FALSE);
1023   if (c != NULL) {
1024     const char *path;
1025 
1026     path = c->argv[0];
1027     if (strcasecmp(path, "none") != 0) {
1028       int res, xerrno;
1029 
1030       pr_signals_block();
1031       PRIVS_ROOT
1032       res = pr_log_openfile(path, &redis_logfd, PR_LOG_SYSTEM_MODE);
1033       xerrno = errno;
1034       PRIVS_RELINQUISH
1035       pr_signals_unblock();
1036 
1037       switch (res) {
1038         case 0:
1039           break;
1040 
1041         case -1:
1042           pr_log_pri(PR_LOG_NOTICE, MOD_REDIS_VERSION
1043             ": notice: unable to open RedisLog '%s': %s", path,
1044             strerror(xerrno));
1045           break;
1046 
1047         case PR_LOG_WRITABLE_DIR:
1048           pr_log_pri(PR_LOG_WARNING, MOD_REDIS_VERSION
1049             ": notice: unable to use RedisLog '%s': parent directory is "
1050               "world-writable", path);
1051           break;
1052 
1053         case PR_LOG_SYMLINK:
1054           pr_log_pri(PR_LOG_WARNING, MOD_REDIS_VERSION
1055             ": notice: unable to use RedisLog '%s': cannot log to a symlink",
1056             path);
1057           break;
1058       }
1059     }
1060   }
1061 
1062   c = find_config(main_server->conf, CONF_PARAM, "RedisOptions", FALSE);
1063   while (c != NULL) {
1064     unsigned long opts = 0;
1065 
1066     pr_signals_handle();
1067 
1068     opts = *((unsigned long *) c->argv[0]);
1069     redis_opts |= opts;
1070 
1071     c = find_config_next(c, c->next, CONF_PARAM, "RedisOptions", FALSE);
1072   }
1073 
1074   c = find_config(main_server->conf, CONF_PARAM, "RedisSentinel", FALSE);
1075   if (c != NULL) {
1076     array_header *sentinels;
1077     const char *master;
1078 
1079     sentinels = c->argv[0];
1080     master = c->argv[1];
1081 
1082     (void) redis_set_sentinels(sentinels, master);
1083   }
1084 
1085   c = find_config(main_server->conf, CONF_PARAM, "RedisServer", FALSE);
1086   if (c != NULL) {
1087     const char *server, *password, *db_idx;
1088     int port;
1089 
1090     server = c->argv[0];
1091     port = *((int *) c->argv[1]);
1092     password = c->argv[2];
1093     db_idx = c->argv[3];
1094 
1095     (void) redis_set_server(server, port, redis_opts, password, db_idx);
1096   }
1097 
1098   c = find_config(main_server->conf, CONF_PARAM, "RedisTimeouts", FALSE);
1099   if (c != NULL) {
1100     unsigned long connect_millis, io_millis;
1101 
1102     connect_millis = *((unsigned long *) c->argv[0]);
1103     io_millis = *((unsigned long *) c->argv[1]);
1104 
1105     if (redis_set_timeouts(connect_millis, io_millis) < 0) {
1106       (void) pr_log_writefile(redis_logfd, MOD_REDIS_VERSION,
1107         "error setting Redis timeouts: %s", strerror(errno));
1108     }
1109   }
1110 
1111   return 0;
1112 }
1113 
1114 /* Module API tables
1115  */
1116 
1117 static conftable redis_conftab[] = {
1118   { "RedisEngine",		set_redisengine,	NULL },
1119   { "RedisLog",			set_redislog,		NULL },
1120   { "RedisLogOnCommand",	set_redislogoncommand,	NULL },
1121   { "RedisLogOnEvent",		set_redislogonevent,	NULL },
1122   { "RedisOptions",		set_redisoptions,	NULL },
1123   { "RedisSentinel",		set_redissentinel,	NULL },
1124   { "RedisServer",		set_redisserver,	NULL },
1125   { "RedisTimeouts",		set_redistimeouts,	NULL },
1126 
1127   { NULL }
1128 };
1129 
1130 static cmdtable redis_cmdtab[] = {
1131   { POST_CMD,		C_PASS,	G_NONE,	redis_post_pass, FALSE,	FALSE },
1132   { LOG_CMD,		C_ANY,	G_NONE,	redis_log_any,	 FALSE,	FALSE },
1133   { LOG_CMD_ERR,	C_ANY,	G_NONE,	redis_log_any,	 FALSE,	FALSE },
1134 
1135   { 0, NULL }
1136 };
1137 
1138 module redis_module = {
1139   NULL, NULL,
1140 
1141   /* Module API version 2.0 */
1142   0x20,
1143 
1144   /* Module name */
1145   "redis",
1146 
1147   /* Module configuration handler table */
1148   redis_conftab,
1149 
1150   /* Module command handler table */
1151   redis_cmdtab,
1152 
1153   /* Module authentication handler table */
1154   NULL,
1155 
1156   /* Module initialization function */
1157   redis_module_init,
1158 
1159   /* Session initialization function */
1160   redis_sess_init,
1161 
1162   /* Module version */
1163   MOD_REDIS_VERSION
1164 };
1165