1 /*
2  * ProFTPD: mod_wrap -- use Wietse Venema's TCP wrappers library for
3  *                      access control
4  * Copyright (c) 2000-2020 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  * -- DO NOT MODIFY THE TWO LINES BELOW --
25  * $Libraries: -lwrap -lnsl$
26  */
27 
28 #define MOD_WRAP_VERSION "mod_wrap/1.2.4"
29 
30 #include "conf.h"
31 #include "privs.h"
32 #include "tcpd.h"
33 
34 /* these need to be defined for the libwrap functions -- default settings
35  * are those from tcpd.h
36  */
37 int allow_severity = PR_LOG_INFO;
38 int deny_severity = PR_LOG_WARNING;
39 
40 module wrap_module;
41 
42 /* Necessary prototypes */
43 static int wrap_eval_expression(char **, array_header *);
44 static const char *wrap_get_user_table(cmd_rec *, const char *, char *);
45 static int wrap_is_usable_file(const char *);
46 static void wrap_log_request_allowed(int, struct request_info *);
47 static void wrap_log_request_denied(int, struct request_info *);
48 static config_rec *wrap_resolve_user(pool *, const char **);
49 static int wrap_sess_init(void);
50 
51 static char *wrap_service_name = "proftpd";
52 
53 /* Support routines
54  */
55 
56 /* boolean "expression" matching, returns TRUE if the entire expression matches
57  */
wrap_eval_expression(char ** config_expr,array_header * session_expr)58 static int wrap_eval_expression(char **config_expr,
59     array_header *session_expr) {
60 
61   unsigned char found = FALSE;
62   unsigned int i = 0;
63   char *elem = NULL, **list = NULL;
64 
65   /* sanity check */
66   if (!config_expr || !*config_expr || !session_expr)
67     return FALSE;
68 
69   list = (char **) session_expr->elts;
70 
71   for (; *config_expr; config_expr++) {
72     elem = *config_expr;
73     found = FALSE;
74 
75     if (*elem == '!') {
76       found = !found;
77       elem++;
78     }
79 
80     for (i = 0; i < session_expr->nelts; i++) {
81       if (list[i] &&
82           strcmp(list[i], elem) == 0) {
83         found = !found;
84         break;
85       }
86     }
87 
88     if (!found) {
89       config_expr = NULL;
90       break;
91     }
92   }
93 
94   if (config_expr)
95     return TRUE;
96 
97   return FALSE;
98 }
99 
wrap_get_user_table(cmd_rec * cmd,const char * user,char * path)100 static const char *wrap_get_user_table(cmd_rec *cmd, const char *user,
101     char *path) {
102   int xerrno = 0;
103 
104   char *real_path = NULL;
105   struct passwd *pw = NULL;
106 
107   pw = pr_auth_getpwnam(cmd->pool, user);
108 
109   /* Handle the case where the given user does not exist. */
110   if (pw == NULL) {
111     return NULL;
112   }
113 
114   /* For the dir_realpath() function to work, some session members need to
115    * be set.
116    */
117   session.user = pstrdup(cmd->pool, pw->pw_name);
118   session.login_uid = pw->pw_uid;
119 
120   PRIVS_USER
121   real_path = dir_realpath(cmd->pool, path);
122   xerrno = errno;
123   PRIVS_RELINQUISH
124 
125   if (real_path) {
126     path = real_path;
127   }
128 
129   errno = xerrno;
130   return path;
131 }
132 
wrap_is_usable_file(const char * filename)133 static int wrap_is_usable_file(const char *filename) {
134   struct stat st;
135   pr_fh_t *fh = NULL;
136 
137   /* check the easy case first */
138   if (filename == NULL) {
139     return FALSE;
140   }
141 
142   /* Make sure that the current process can _read_ the file. */
143   fh = pr_fsio_open(filename, O_RDONLY);
144   if (fh == NULL) {
145     int xerrno = errno;
146 
147     pr_log_pri(PR_LOG_NOTICE, MOD_WRAP_VERSION ": failed to read \"%s\": %s",
148       filename, strerror(xerrno));
149 
150     errno = xerrno;
151     return FALSE;
152   }
153 
154   if (pr_fsio_fstat(fh, &st) < 0) {
155     int xerrno = errno;
156 
157     pr_log_pri(PR_LOG_NOTICE, MOD_WRAP_VERSION ": failed to stat \"%s\": %s",
158       filename, strerror(xerrno));
159 
160     pr_fsio_close(fh);
161     errno = xerrno;
162     return FALSE;
163   }
164 
165   if (S_ISDIR(st.st_mode)) {
166     int xerrno = EISDIR;
167 
168     pr_log_pri(PR_LOG_NOTICE, MOD_WRAP_VERSION ": unable to use \"%s\": %s",
169       filename, strerror(xerrno));
170 
171     pr_fsio_close(fh);
172     errno = xerrno;
173     return FALSE;
174   }
175 
176   pr_fsio_close(fh);
177   return TRUE;
178 }
179 
wrap_log_request_allowed(int severity,struct request_info * request)180 static void wrap_log_request_allowed(int severity,
181     struct request_info *request) {
182   int priority;
183 
184   /* Mask off the facility bits. */
185   priority = (severity & PR_LOG_PRIMASK);
186 
187   pr_log_pri(priority, MOD_WRAP_VERSION ": allowed connection from %s",
188     eval_client(request));
189 
190   /* done */
191   return;
192 }
193 
wrap_log_request_denied(int severity,struct request_info * request)194 static void wrap_log_request_denied(int severity,
195     struct request_info *request) {
196   int priority;
197 
198   /* Mask off the facility bits. */
199   priority = (severity & PR_LOG_PRIMASK);
200 
201   pr_log_pri(priority, MOD_WRAP_VERSION ": refused connection from %s",
202     eval_client(request));
203 
204   /* done */
205   return;
206 }
207 
wrap_resolve_user(pool * p,const char ** user)208 static config_rec *wrap_resolve_user(pool *p, const char **user) {
209   config_rec *conf = NULL, *top_conf;
210   char *ourname = NULL, *anonname = NULL;
211   unsigned char is_alias = FALSE, force_anon = FALSE;
212 
213   /* Precedence rules:
214    *   1. Search for UserAlias directive.
215    *   2. Search for Anonymous directive.
216    *   3. Normal user login
217    */
218 
219   ourname = (char*) get_param_ptr(main_server->conf, "UserName", FALSE);
220 
221   conf = find_config(main_server->conf, CONF_PARAM, "UserAlias", TRUE);
222 
223   if (conf) do {
224     if (strcmp(conf->argv[0], "*") == 0 ||
225         strcmp(conf->argv[0], *user) == 0) {
226       is_alias = TRUE;
227       break;
228     }
229 
230   } while ((conf = find_config_next(conf, conf->next, CONF_PARAM,
231     "UserAlias", TRUE)) != NULL);
232 
233   /* if AuthAliasOnly is set, ignore this one and continue */
234   top_conf = conf;
235 
236   while (conf && conf->parent &&
237       find_config(conf->parent->set, CONF_PARAM, "AuthAliasOnly", FALSE)) {
238 
239     is_alias = FALSE;
240     find_config_set_top(top_conf);
241     conf = find_config_next(conf, conf->next, CONF_PARAM, "UserAlias", TRUE);
242 
243     if (conf &&
244         (strcmp(conf->argv[0], "*") == 0 ||
245          strcmp(conf->argv[0], *user) == 0))
246       is_alias = TRUE;
247   }
248 
249   if (conf != NULL) {
250     *user = conf->argv[1];
251 
252     /* If the alias is applied inside an <Anonymous> context, we have found
253      * our anon block
254      */
255     if (conf->parent &&
256         conf->parent->config_type == CONF_ANON) {
257       conf = conf->parent;
258 
259     } else {
260       conf = NULL;
261     }
262   }
263 
264   /* Next, search for an anonymous entry */
265   if (conf == NULL) {
266     conf = find_config(main_server->conf, CONF_ANON, NULL, FALSE);
267 
268   } else {
269     find_config_set_top(conf);
270   }
271 
272   if (conf != NULL) do {
273     anonname = (char*) get_param_ptr(conf->subset, "UserName", FALSE);
274 
275     if (!anonname)
276       anonname = ourname;
277 
278     if (anonname &&
279         strcmp(anonname, *user) == 0) {
280        break;
281     }
282 
283   } while ((conf = find_config_next(conf, conf->next, CONF_ANON, NULL,
284     FALSE)) != NULL);
285 
286   if (!is_alias && !force_anon) {
287 
288     if (find_config((conf ? conf->subset :
289         main_server->conf), CONF_PARAM, "AuthAliasOnly", FALSE)) {
290 
291       if (conf != NULL &&
292           conf->config_type == CONF_ANON) {
293         conf = NULL;
294 
295       } else {
296         *user = NULL;
297       }
298 
299       if (*user != NULL &&
300           find_config(main_server->conf, CONF_PARAM, "AuthAliasOnly", FALSE)) {
301         *user = NULL;
302       }
303     }
304   }
305 
306   return conf;
307 }
308 
309 /* Configuration handlers
310  */
311 
set_tcpaccessfiles(cmd_rec * cmd)312 MODRET set_tcpaccessfiles(cmd_rec *cmd) {
313   config_rec *c = NULL;
314 
315   /* assume use of the standard TCP wrappers installation locations */
316   char *allow_filename = "/etc/hosts.allow";
317   char *deny_filename = "/etc/hosts.deny";
318 
319   CHECK_ARGS(cmd, 2);
320   CHECK_CONF(cmd, CONF_ROOT|CONF_ANON|CONF_VIRTUAL|CONF_GLOBAL);
321 
322   /* use the user-given files, checking to make sure that they exist and
323    * are readable.
324    */
325   allow_filename = cmd->argv[1];
326   deny_filename = cmd->argv[2];
327 
328   /* if the filenames begin with a '~', AND this is not immediately followed
329    * by a '/' (ie '~/'), expand it out for checking and storing for later
330    * lookups.  If the filenames DO begin with '~/', do the expansion later,
331    * after authenication.  In other words, do checking of static filenames
332    * now, and checking of dynamic (user-authentication-based) filenames
333    * later.
334    */
335   if (allow_filename[0] == '/') {
336 
337     /* it's an absolute path, so the filename will be checked as is */
338     if (!wrap_is_usable_file(allow_filename))
339       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
340         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
341 
342   } else if (allow_filename[0] == '~' && allow_filename[1] != '/') {
343     char *allow_real_file = NULL;
344 
345     allow_real_file = dir_realpath(cmd->pool, allow_filename);
346 
347     if (allow_real_file == NULL || !wrap_is_usable_file(allow_real_file))
348       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
349         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
350 
351     allow_filename = allow_real_file;
352 
353   } else if (allow_filename[0] != '~' && allow_filename[0] != '/') {
354 
355     /* no relative paths allowed */
356     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
357       cmd->argv[0], ": '", allow_filename, "' must start with \"/\" or \"~\"",
358       NULL));
359 
360   } else {
361 
362     /* it's a determine-at-login-time filename -- check it later */
363     ;
364   }
365 
366   if (deny_filename[0] == '/') {
367 
368     /* it's an absolute path, so the filename will be checked as is */
369     if (!wrap_is_usable_file(deny_filename))
370       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
371         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
372 
373   } else if (deny_filename[0] == '~' && deny_filename[1] != '/') {
374     char *deny_real_file = NULL;
375 
376     deny_real_file = dir_realpath(cmd->pool, deny_filename);
377 
378     if (deny_real_file == NULL || !wrap_is_usable_file(deny_real_file))
379       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
380         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
381 
382     deny_filename = deny_real_file;
383 
384   } else if (deny_filename[0] != '~' && deny_filename[0] != '/') {
385 
386     /* no relative paths allowed */
387     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
388       cmd->argv[0], ": '", deny_filename, "' must start with \"/\" or \"~\"",
389       NULL));
390 
391   } else {
392 
393     /* it's a determine-at-login-time filename -- check it later */
394     ;
395   }
396 
397   c = add_config_param_str(cmd->argv[0], 2, (void *) allow_filename,
398     (void *) deny_filename);
399   c->flags |= CF_MERGEDOWN;
400 
401   /* done */
402   return PR_HANDLED(cmd);
403 }
404 
set_tcpgroupaccessfiles(cmd_rec * cmd)405 MODRET set_tcpgroupaccessfiles(cmd_rec *cmd) {
406   unsigned int group_argc = 1;
407   char *expr, **group_argv = NULL;
408   array_header *group_acl = NULL;
409   config_rec *c = NULL;
410 
411   /* assume use of the standard TCP wrappers installation locations */
412   char *allow_filename = NULL, *deny_filename = NULL;
413 
414   CHECK_ARGS(cmd, 3);
415   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
416 
417   /* use the user-given files, checking to make sure that they exist and
418    * are readable.
419    */
420   allow_filename = cmd->argv[2];
421   deny_filename = cmd->argv[3];
422 
423   /* if the filenames begin with a '~', AND this is not immediately followed
424    * by a '/' (ie '~/'), expand it out for checking and storing for later
425    * lookups.  If the filenames DO begin with '~/', do the expansion later,
426    * after authenication.  In other words, do checking of static filenames
427    * now, and checking of dynamic (user-authentication-based) filenames
428    * later.
429    */
430   if (allow_filename[0] == '/') {
431 
432     /* it's an absolute path, so the filename will be checked as is */
433     if (!wrap_is_usable_file(allow_filename))
434       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
435         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
436 
437   } else if (allow_filename[0] == '~' && allow_filename[1] != '/') {
438     char *allow_real_file = NULL;
439 
440     allow_real_file = dir_realpath(cmd->pool, allow_filename);
441 
442     if (allow_real_file == NULL || !wrap_is_usable_file(allow_real_file))
443       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
444         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
445 
446     allow_filename = allow_real_file;
447 
448   } else if (allow_filename[0] != '~' && allow_filename[0] != '/') {
449 
450     /* no relative paths allowed */
451     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
452       cmd->argv[0], ": '", allow_filename, "' must start with \"/\" or \"~\"",
453       NULL));
454 
455   } else {
456 
457     /* it's a determine-at-login-time filename -- check it later */
458     ;
459   }
460 
461   if (deny_filename[0] == '/') {
462 
463     /* it's an absolute path, so the filename will be checked as is */
464     if (!wrap_is_usable_file(deny_filename))
465       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
466         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
467 
468   } else if (deny_filename[0] == '~' && deny_filename[1] != '/') {
469     char *deny_real_file = NULL;
470 
471     deny_real_file = dir_realpath(cmd->pool, deny_filename);
472 
473     if (deny_real_file == NULL || !wrap_is_usable_file(deny_real_file))
474       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
475         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
476 
477     deny_filename = deny_real_file;
478 
479   } else if (deny_filename[0] != '~' && deny_filename[0] != '/') {
480 
481     /* no relative paths allowed */
482     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
483       cmd->argv[0], ": '", deny_filename, "' must start with \"/\" or \"~\"",
484       NULL));
485 
486   } else {
487 
488     /* it's a determine-at-login-time filename -- check it later */
489     ;
490   }
491 
492   c = add_config_param(cmd->argv[0], 0);
493 
494   expr = (char *) cmd->argv[0];
495   group_acl = pr_expr_create(cmd->tmp_pool, &group_argc, &expr);
496 
497   /* build the desired config_rec manually */
498   c->argc = group_argc + 2;
499   c->argv = pcalloc(c->pool, (group_argc + 3) * sizeof(char *));
500   group_argv = (char **) c->argv;
501 
502   /* the access files are the first two arguments */
503   *group_argv++ = pstrdup(c->pool, allow_filename);
504   *group_argv++ = pstrdup(c->pool, deny_filename);
505 
506   /* and the group names follow */
507   if (group_argc && group_acl)
508     while (group_argc--) {
509       *group_argv++ = pstrdup(c->pool, *((char **) group_acl->elts));
510       group_acl->elts = ((char **) group_acl->elts) + 1;
511     }
512 
513   /* don't forget to NULL-terminate */
514   *group_argv = NULL;
515 
516   c->flags |= CF_MERGEDOWN;
517 
518   /* done */
519   return PR_HANDLED(cmd);
520 }
521 
set_tcpuseraccessfiles(cmd_rec * cmd)522 MODRET set_tcpuseraccessfiles(cmd_rec *cmd) {
523   unsigned int user_argc = 1;
524   char *expr, **user_argv = NULL;
525   array_header *user_acl = NULL;
526   config_rec *c = NULL;
527 
528   /* assume use of the standard TCP wrappers installation locations */
529   char *allow_filename = NULL, *deny_filename = NULL;
530 
531   CHECK_ARGS(cmd, 3);
532   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
533 
534   /* use the user-given files, checking to make sure that they exist and
535    * are readable.
536    */
537   allow_filename = cmd->argv[2];
538   deny_filename = cmd->argv[3];
539 
540   /* if the filenames begin with a '~', AND this is not immediately followed
541    * by a '/' (ie '~/'), expand it out for checking and storing for later
542    * lookups.  If the filenames DO begin with '~/', do the expansion later,
543    * after authenication.  In other words, do checking of static filenames
544    * now, and checking of dynamic (user-authentication-based) filenames
545    * later.
546    */
547   if (allow_filename[0] == '/') {
548 
549     /* it's an absolute path, so the filename will be checked as is */
550     if (!wrap_is_usable_file(allow_filename))
551       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
552         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
553 
554   } else if (allow_filename[0] == '~' && allow_filename[1] != '/') {
555     char *allow_real_file = NULL;
556 
557     allow_real_file = dir_realpath(cmd->pool, allow_filename);
558 
559     if (allow_real_file == NULL || !wrap_is_usable_file(allow_real_file))
560       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
561         cmd->argv[0], ": '", allow_filename, "' must be a usable file", NULL));
562 
563     allow_filename = allow_real_file;
564 
565   } else if (allow_filename[0] != '~' && allow_filename[0] != '/') {
566 
567     /* no relative paths allowed */
568     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
569       cmd->argv[0], ": '", allow_filename, "' must start with \"/\" or \"~\"",
570       NULL));
571 
572   } else {
573 
574     /* it's a determine-at-login-time filename -- check it later */
575     ;
576   }
577 
578   if (deny_filename[0] == '/') {
579 
580     /* it's an absolute path, so the filename will be checked as is */
581     if (!wrap_is_usable_file(deny_filename))
582       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
583         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
584 
585   } else if (deny_filename[0] == '~' && deny_filename[1] != '/') {
586     char *deny_real_file = NULL;
587 
588     deny_real_file = dir_realpath(cmd->pool, deny_filename);
589 
590     if (deny_real_file == NULL || !wrap_is_usable_file(deny_real_file))
591       return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
592         cmd->argv[0], ": '", deny_filename, "' must be a usable file", NULL));
593 
594     deny_filename = deny_real_file;
595 
596   } else if (deny_filename[0] != '~' && deny_filename[0] != '/') {
597 
598     /* no relative paths allowed */
599     return PR_ERROR_MSG(cmd, NULL, pstrcat(cmd->tmp_pool,
600       cmd->argv[0], ": '", deny_filename, "' must start with \"/\" or \"~\"",
601       NULL));
602 
603   } else {
604 
605     /* it's a determine-at-login-time filename -- check it later */
606     ;
607   }
608 
609   c = add_config_param_str(cmd->argv[0], 0);
610 
611   expr = (char *) cmd->argv[0];
612   user_acl = pr_expr_create(cmd->tmp_pool, &user_argc, &expr);
613 
614   /* build the desired config_rec manually */
615   c->argc = user_argc + 2;
616   c->argv = pcalloc(c->pool, (user_argc + 3) * sizeof(char *));
617   user_argv = (char **) c->argv;
618 
619   /* the access files are the first two arguments */
620   *user_argv++ = pstrdup(c->pool, allow_filename);
621   *user_argv++ = pstrdup(c->pool, deny_filename);
622 
623   /* and the user names follow */
624   if (user_argc && user_acl)
625     while (user_argc--) {
626       *user_argv++ = pstrdup(c->pool, *((char **) user_acl->elts));
627       user_acl->elts = ((char **) user_acl->elts) + 1;
628     }
629 
630   /* don't forget to NULL-terminate */
631   *user_argv = NULL;
632 
633   c->flags |= CF_MERGEDOWN;
634 
635   /* done */
636   return PR_HANDLED(cmd);
637 }
638 
639 /* This function was copied, almost verbatim, from the set_sysloglevel()
640  * function in modules/mod_core.c.  I hereby cite the source for this code
641  * as MacGuyver <macguyver@tos.net>. =)
642  */
set_tcpaccesssysloglevels(cmd_rec * cmd)643 MODRET set_tcpaccesssysloglevels(cmd_rec *cmd) {
644   config_rec *c = NULL;
645   int allow_level = PR_LOG_DEBUG, deny_level = PR_LOG_DEBUG;
646 
647   CHECK_ARGS(cmd, 2);
648   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_ANON|CONF_GLOBAL);
649 
650   if (strcasecmp(cmd->argv[1], "emerg") == 0) {
651     allow_level = PR_LOG_EMERG;
652 
653   } else if (strcasecmp(cmd->argv[1], "alert") == 0) {
654     allow_level = PR_LOG_ALERT;
655 
656   } else if (strcasecmp(cmd->argv[1], "crit") == 0) {
657     allow_level = PR_LOG_CRIT;
658 
659   } else if (strcasecmp(cmd->argv[1], "error") == 0) {
660     allow_level = PR_LOG_ERR;
661 
662   } else if (strcasecmp(cmd->argv[1], "warn") == 0) {
663     allow_level = PR_LOG_WARNING;
664 
665   } else if (strcasecmp(cmd->argv[1], "notice") == 0) {
666     allow_level = PR_LOG_NOTICE;
667 
668   } else if (strcasecmp(cmd->argv[1], "info") == 0) {
669     allow_level = PR_LOG_INFO;
670 
671   } else if (strcasecmp(cmd->argv[1], "debug") == 0) {
672     allow_level = PR_LOG_DEBUG;
673 
674   } else {
675     CONF_ERROR(cmd, "TCPAccessSyslogLevels requires \"allow\" level keyword: "
676       "one of emerg/alert/crit/error/warn/notice/info/debug");
677   }
678 
679   if (strcasecmp(cmd->argv[2], "emerg") == 0) {
680     deny_level = PR_LOG_EMERG;
681 
682   } else if (strcasecmp(cmd->argv[2], "alert") == 0) {
683     deny_level = PR_LOG_ALERT;
684 
685   } else if (strcasecmp(cmd->argv[2], "crit") == 0) {
686     deny_level = PR_LOG_CRIT;
687 
688   } else if (strcasecmp(cmd->argv[2], "error") == 0) {
689     deny_level = PR_LOG_ERR;
690 
691   } else if (strcasecmp(cmd->argv[2], "warn") == 0) {
692     deny_level = PR_LOG_WARNING;
693 
694   } else if (strcasecmp(cmd->argv[2], "notice") == 0) {
695     deny_level = PR_LOG_NOTICE;
696 
697   } else if (strcasecmp(cmd->argv[2], "info") == 0) {
698     deny_level = PR_LOG_INFO;
699 
700   } else if (strcasecmp(cmd->argv[2], "debug") == 0 ) {
701     deny_level = PR_LOG_DEBUG;
702 
703   } else {
704     CONF_ERROR(cmd, "TCPAccessSyslogLevels requires \"deny\" level keyword: "
705       "one of emerg/alert/crit/error/warn/notice/info/debug");
706   }
707 
708   c = add_config_param(cmd->argv[0], 2, NULL, NULL);
709   c->argv[0] = palloc(c->pool, sizeof(int));
710   *((int *) c->argv[0]) = allow_level;
711   c->argv[1] = palloc(c->pool, sizeof(int));
712   *((int *) c->argv[1]) = deny_level;
713 
714   c->flags |= CF_MERGEDOWN;
715 
716   return PR_HANDLED(cmd);
717 }
718 
719 /* usage: TCPServiceName <name> */
set_tcpservicename(cmd_rec * cmd)720 MODRET set_tcpservicename(cmd_rec *cmd) {
721   CHECK_ARGS(cmd, 1);
722   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
723 
724   add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
725   return PR_HANDLED(cmd);
726 }
727 
728 /* Command handlers
729  */
730 
wrap_handle_request(cmd_rec * cmd)731 MODRET wrap_handle_request(cmd_rec *cmd) {
732 
733   /* these variables are names expected to be set by the TCP wrapper code
734    */
735   struct request_info request;
736 
737   const char *user = NULL;
738   config_rec *conf = NULL, *access_conf = NULL, *syslog_conf = NULL;
739   hosts_allow_table = NULL;
740   hosts_deny_table = NULL;
741 
742   /* hide passwords */
743   session.hide_password = TRUE;
744 
745   /* Sneaky...found in mod_auth.c's cmd_pass() function.  Need to find the
746    * login UID in order to resolve the possibly-login-dependent filename.
747    */
748   user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
749 
750   /* It's possible that a PASS command came before USER.  This is a PRE_CMD
751    * handler, so it won't be protected from this case; we'll need to do
752    * it manually.
753    */
754   if (user == NULL) {
755     return PR_DECLINED(cmd);
756   }
757 
758   /* Use mod_auth's _auth_resolve_user() [imported for use here] to get the
759    * right configuration set, since the user may be logging in anonymously,
760    * and the session struct hasn't yet been set for that yet (thus short-
761    * circuiting the easiest way to get the right context...the macros.
762    */
763   conf = wrap_resolve_user(cmd->pool, &user);
764 
765   /* Search first for user-specific access files.  Multiple TCPUserAccessFiles
766    * directives are allowed.
767    */
768   if ((access_conf = find_config(conf ? conf->subset : CURRENT_CONF, CONF_PARAM,
769       "TCPUserAccessFiles", FALSE)) != NULL) {
770     int matched = FALSE;
771     array_header *user_array = NULL;
772 
773     while (access_conf) {
774 
775       user_array = make_array(cmd->tmp_pool, 0, sizeof(char *));
776       *((char **) push_array(user_array)) = pstrdup(cmd->tmp_pool, user);
777 
778       /* Check the user expression -- don't forget the offset, to skip
779        * the access file name strings in argv
780        */
781       if (wrap_eval_expression(((char **) access_conf->argv) + 2,
782           user_array)) {
783         pr_log_debug(DEBUG4, MOD_WRAP_VERSION
784           ": matched TCPUserAccessFiles expression");
785         matched = TRUE;
786         break;
787       }
788 
789       access_conf = find_config_next(access_conf, access_conf->next,
790         CONF_PARAM, "TCPUserAccessFiles", FALSE);
791     }
792 
793     if (!matched)
794       access_conf = NULL;
795   }
796 
797   /* Next, search for group-specific access files.  Multiple
798    * TCPGroupAccessFiles directives are allowed.
799    */
800   if (!access_conf && (access_conf = find_config(conf ? conf->subset :
801         CURRENT_CONF, CONF_PARAM, "TCPGroupAccessFiles", FALSE)) != NULL) {
802     unsigned char matched = FALSE;
803 
804     /* NOTE: this gid_array is only necessary until Bug#1461 is fixed */
805     array_header *gid_array = make_array(cmd->pool, 0, sizeof(gid_t));
806 
807     array_header *group_array = make_array(cmd->pool, 0, sizeof(char *));
808 
809     while (access_conf) {
810       if (pr_auth_getgroups(cmd->pool, user, &gid_array, &group_array) < 1) {
811         pr_log_debug(DEBUG3, MOD_WRAP_VERSION
812           ": no supplemental groups found for user '%s'", user);
813 
814       } else {
815 
816         /* Check the group expression -- don't forget the offset, to skip
817          * the access file names strings in argv
818          */
819         if (wrap_eval_expression(((char **) access_conf->argv) + 2,
820             group_array)) {
821           pr_log_debug(DEBUG4, MOD_WRAP_VERSION
822             ": matched TCPGroupAccessFiles expression");
823           matched = TRUE;
824           break;
825         }
826       }
827 
828       access_conf = find_config_next(access_conf, access_conf->next,
829         CONF_PARAM, "TCPGroupAccessFiles", FALSE);
830     }
831 
832     if (!matched)
833       access_conf = NULL;
834   }
835 
836   /* Finally for globally-applicable access files.  Only one such directive
837    * is allowed.
838    */
839   if (!access_conf) {
840     access_conf = find_config(conf ? conf->subset : CURRENT_CONF,
841       CONF_PARAM, "TCPAccessFiles", FALSE);
842   }
843 
844   if (access_conf) {
845     hosts_allow_table = (char *) access_conf->argv[0];
846     hosts_deny_table = (char *) access_conf->argv[1];
847   }
848 
849   /* Now, check the retrieved filename, and see if it requires a login-time
850    * file.
851    */
852   if (hosts_allow_table != NULL && hosts_allow_table[0] == '~' &&
853       hosts_allow_table[1] == '/') {
854     const char *allow_real_table = NULL;
855 
856     allow_real_table = wrap_get_user_table(cmd, user, hosts_allow_table);
857 
858     if (!wrap_is_usable_file(allow_real_table)) {
859       pr_log_pri(PR_LOG_WARNING, MOD_WRAP_VERSION
860         ": configured TCPAllowFile %s is unusable", hosts_allow_table);
861       hosts_allow_table = NULL;
862 
863     } else
864       hosts_allow_table = (char *) allow_real_table;
865   }
866 
867   if (hosts_deny_table != NULL && hosts_deny_table[0] == '~' &&
868       hosts_deny_table[1] == '/') {
869     char *deny_real_table = NULL;
870 
871     deny_real_table = dir_realpath(cmd->pool, hosts_deny_table);
872 
873     if (!wrap_is_usable_file(deny_real_table)) {
874       pr_log_pri(PR_LOG_WARNING, MOD_WRAP_VERSION
875         ": configured TCPDenyFile %s is unusable", hosts_deny_table);
876       hosts_deny_table = NULL;
877 
878     } else
879       hosts_deny_table = deny_real_table;
880   }
881 
882   /* Make sure that _both_ allow and deny TCPAccessFiles are present.
883    * If not, log the missing file, and by default allow request to succeed.
884    */
885   if (hosts_allow_table != NULL && hosts_deny_table != NULL) {
886 
887     /* Most common case...nothing more necessary */
888 
889   } else if (hosts_allow_table == NULL && hosts_deny_table != NULL) {
890 
891     /* Log the missing file */
892     pr_log_pri(PR_LOG_INFO, MOD_WRAP_VERSION ": no usable allow access file -- "
893       "allowing connection");
894 
895     return PR_DECLINED(cmd);
896 
897   } else if (hosts_allow_table != NULL && hosts_deny_table == NULL) {
898 
899     /* log the missing file */
900     pr_log_pri(PR_LOG_INFO, MOD_WRAP_VERSION ": no usable deny access file -- "
901       "allowing connection");
902 
903     return PR_DECLINED(cmd);
904 
905   } else {
906 
907     /* Neither set -- assume the admin hasn't configured these directives
908      * at all.
909      */
910     return PR_DECLINED(cmd);
911   }
912 
913   /* Log the names of the allow/deny files being used. */
914   pr_log_pri(PR_LOG_DEBUG, MOD_WRAP_VERSION ": using access files: %s, %s",
915     hosts_allow_table, hosts_deny_table);
916 
917   /* retrieve the user-defined syslog priorities, if any.  Fall back to the
918    * defaults as seen in tcpd.h if not defined.
919    */
920   syslog_conf = find_config(main_server->conf, CONF_PARAM,
921     "TCPAccessSyslogLevels", FALSE);
922 
923   if (syslog_conf) {
924     allow_severity = *((int *) syslog_conf->argv[0]);
925     deny_severity = *((int *) syslog_conf->argv[1]);
926 
927   } else {
928     allow_severity = PR_LOG_INFO;
929     deny_severity = PR_LOG_WARNING;
930   }
931 
932   /* While it may look odd to OR together the syslog facility and level,
933    * that is the way that syslog(3) says to do it:
934    *
935    *  "The priority argument is formed by ORing the facility and the level
936    *   values..."
937    *
938    * Note that we do this OR here because the allow_severity/deny_severity
939    * values are ALSO used by the libwrap library; it is also why we need
940    * to mask off some bits later, when using proftpd's logging functions.
941    */
942   allow_severity = log_getfacility() | allow_severity;
943   deny_severity = log_getfacility() | deny_severity;
944 
945   pr_log_debug(DEBUG4, MOD_WRAP_VERSION ": checking under service name '%s'",
946     wrap_service_name);
947   request_init(&request, RQ_DAEMON, wrap_service_name, RQ_FILE,
948     session.c->rfd, 0);
949 
950   fromhost(&request);
951 
952   if (STR_EQ(eval_hostname(request.client), paranoid) ||
953       !hosts_access(&request)) {
954     char *denymsg = NULL;
955 
956     /* log the denied connection */
957     wrap_log_request_denied(deny_severity, &request);
958 
959     /* Broadcast this event to any interested listeners. */
960     pr_event_generate("mod_wrap.connection-denied", NULL);
961 
962     /* check for AccessDenyMsg */
963     denymsg = (char *) get_param_ptr(TOPLEVEL_CONF, "AccessDenyMsg", FALSE);
964     if (denymsg != NULL) {
965       denymsg = (char *) sreplace(cmd->tmp_pool, denymsg, "%u", user, NULL);
966     }
967 
968     if (denymsg != NULL) {
969       return PR_ERROR_MSG(cmd, R_530, denymsg);
970     }
971 
972     return PR_ERROR_MSG(cmd, R_530, _("Access denied"));
973   }
974 
975   /* If request is allowable, return DECLINED (for engine to act as if this
976    * handler was never called, else ERROR (for engine to abort processing and
977    * deny request.
978    */
979   wrap_log_request_allowed(allow_severity, &request);
980 
981   return PR_DECLINED(cmd);
982 }
983 
984 /* Event listeners
985  */
986 
wrap_sess_reinit_ev(const void * event_data,void * user_data)987 static void wrap_sess_reinit_ev(const void *event_data, void *user_data) {
988   int res;
989 
990   /* A HOST command changed the main_server pointer; reinitialize ourselves. */
991 
992   pr_event_unregister(&wrap_module, "core.session-reinit", wrap_sess_reinit_ev);
993 
994   /* Reset defaults */
995   wrap_service_name = "proftpd";
996 
997   res = wrap_sess_init();
998   if (res < 0) {
999     pr_session_disconnect(&wrap_module, PR_SESS_DISCONNECT_SESSION_INIT_FAILED,
1000       NULL);
1001   }
1002 }
1003 
1004 /* Initialization routines
1005  */
1006 
wrap_sess_init(void)1007 static int wrap_sess_init(void) {
1008   pr_event_register(&wrap_module, "core.session-reinit", wrap_sess_reinit_ev,
1009     NULL);
1010 
1011   /* look up any configured TCPServiceName */
1012   wrap_service_name = get_param_ptr(main_server->conf, "TCPServiceName", FALSE);
1013   if (wrap_service_name == NULL) {
1014     wrap_service_name = "proftpd";
1015   }
1016 
1017   return 0;
1018 }
1019 
1020 /* Module API tables
1021  */
1022 
1023 static conftable wrap_conftab[] = {
1024   { "TCPAccessFiles",        set_tcpaccessfiles,        NULL },
1025   { "TCPAccessSyslogLevels", set_tcpaccesssysloglevels, NULL },
1026   { "TCPGroupAccessFiles",   set_tcpgroupaccessfiles,   NULL },
1027   { "TCPServiceName",	     set_tcpservicename,	NULL },
1028   { "TCPUserAccessFiles",    set_tcpuseraccessfiles,    NULL },
1029   { NULL }
1030 };
1031 
1032 static cmdtable wrap_cmdtab[] = {
1033   { PRE_CMD, C_PASS, G_NONE, wrap_handle_request, FALSE, FALSE },
1034   { 0, NULL }
1035 };
1036 
1037 module wrap_module = {
1038   NULL, NULL,
1039 
1040   /* Module API version 2.0 */
1041   0x20,
1042 
1043   /* Module name */
1044   "wrap",
1045 
1046   /* Mmodule configuration handler table */
1047   wrap_conftab,
1048 
1049   /* Module command handler table */
1050   wrap_cmdtab,
1051 
1052   /* Module authentication handler table */
1053   NULL,
1054 
1055   /* Module initialization */
1056   NULL,
1057 
1058   /* Session initialization */
1059   wrap_sess_init,
1060 
1061   /* Module version */
1062   MOD_WRAP_VERSION
1063 };
1064