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