1 /*
2  * ProFTPD: mod_wrap2_file -- a mod_wrap2 sub-module for supplying IP-based
3  *                            access control data via file-based tables
4  * Copyright (c) 2002-2016 TJ Saunders
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19  *
20  * As a special exemption, TJ Saunders gives permission to link this program
21  * with OpenSSL, and distribute the resulting executable, without including
22  * the source code for OpenSSL in the source distribution.
23  */
24 
25 #include "mod_wrap2.h"
26 
27 #define MOD_WRAP2_FILE_VERSION		"mod_wrap2_file/1.3"
28 
29 module wrap2_file_module;
30 
31 static const char *filetab_service_name = NULL;
32 
33 static array_header *filetab_clients_list = NULL;
34 static array_header *filetab_daemons_list = NULL;
35 static array_header *filetab_options_list = NULL;
36 
37 #ifndef MOD_WRAP2_FILE_BUFFER_SIZE
38 # define MOD_WRAP2_FILE_BUFFER_SIZE	PR_TUNABLE_BUFFER_SIZE
39 #endif
40 
filetab_parse_table(wrap2_table_t * filetab)41 static void filetab_parse_table(wrap2_table_t *filetab) {
42   unsigned int lineno = 0;
43   char buf[MOD_WRAP2_FILE_BUFFER_SIZE] = {'\0'};
44 
45   while (pr_fsio_getline(buf, sizeof(buf), (pr_fh_t *) filetab->tab_handle,
46       &lineno) != NULL) {
47     char *ptr, *res = NULL, *service = NULL;
48     size_t buflen = strlen(buf);
49 
50     if (buf[buflen-1] != '\n') {
51       wrap2_log("file '%s': missing newline or line too long (%u) at line %u",
52         filetab->tab_name, (unsigned int) buflen, lineno);
53       continue;
54     }
55 
56     if (buf[0] == '#' || buf[strspn(buf, " \t\r\n")] == 0) {
57       continue;
58     }
59 
60     buf[buflen-1] = '\0';
61 
62     /* The list of daemons is from the start of the line to a ':' delimiter.
63      * This list is assumed to be space-delimited; failure to match this
64      * syntax will result in lack of desired results when doing the access
65      * checks.
66      */
67     ptr = strchr(buf, ':');
68     if (ptr == NULL) {
69       wrap2_log("file '%s': badly formatted list of daemon/service names at "
70         "line %u", filetab->tab_name, lineno);
71       continue;
72     }
73 
74     service = pstrndup(filetab->tab_pool, buf, (ptr - buf));
75 
76     if (filetab_service_name &&
77         (strcasecmp(filetab_service_name, service) == 0 ||
78          strncasecmp("ALL", service, 4) == 0)) {
79       if (filetab_daemons_list == NULL) {
80         filetab_daemons_list = make_array(filetab->tab_pool, 0, sizeof(char *));
81       }
82 
83       *((char **) push_array(filetab_daemons_list)) = service;
84 
85       res = wrap2_strsplit(buf, ':');
86       if (res == NULL) {
87         wrap2_log("file '%s': missing \":\" separator at %u",
88           filetab->tab_name, lineno);
89         continue;
90       }
91 
92       if (filetab_clients_list == NULL) {
93         filetab_clients_list = make_array(filetab->tab_pool, 0, sizeof(char *));
94       }
95 
96       /* Check for another ':' delimiter.  If present, anything following that
97        * delimiter is an option/shell command (as per the hosts_access(5) man
98        * page syntax description).
99        *
100        * If there are commas or whitespace in the line, parse them as separate
101        * client names.  Otherwise, a comma- or space-delimited list of names
102        * will be treated as a single name, and violate the principle of least
103        * surprise for the site admin.
104        *
105        * NOTE: Disable support for options in the file syntax if IPv6 addresses
106        * are present, since the parsing code below is not sufficient for
107        * handling both IPv6 addresses AND options, e.g.:
108        *
109        *  proftpd: [::1] [::2]: <options>
110        */
111 
112       ptr = strchr(res, ':');
113       if (ptr != NULL) {
114         char *clients;
115         size_t clients_len;
116 
117         clients_len = (ptr - res);
118         clients = pstrndup(filetab->tab_pool, res, clients_len);
119 
120         if (strcspn(clients, "[]") == clients_len) {
121           ptr = wrap2_strsplit(res, ':');
122 
123           if (filetab_options_list == NULL) {
124             filetab_options_list = make_array(filetab->tab_pool, 0,
125               sizeof(char *));
126           }
127 
128           /* Skip redundant whitespaces */
129           while (*ptr == ' ' ||
130                  *ptr == '\t') {
131             pr_signals_handle();
132             ptr++;
133           }
134 
135           *((char **) push_array(filetab_options_list)) =
136             pstrdup(filetab->tab_pool, ptr);
137 
138         } else {
139           /* Ignoring options and IPv6 addresses (Bug#4090) for now. */
140         }
141 
142       } else {
143         /* No options present. */
144         ptr = res;
145       }
146 
147       ptr = strpbrk(res, ", \t");
148       if (ptr != NULL) {
149         char *dup_opts, *word;
150 
151         dup_opts = pstrdup(filetab->tab_pool, res);
152         while ((word = pr_str_get_token(&dup_opts, ", \t")) != NULL) {
153           size_t wordlen;
154 
155           pr_signals_handle();
156 
157           wordlen = strlen(word);
158           if (wordlen == 0) {
159             continue;
160           }
161 
162           /* Remove any trailing comma */
163           if (word[wordlen-1] == ',') {
164             word[wordlen-1] = '\0';
165             wordlen--;
166           }
167 
168           *((char **) push_array(filetab_clients_list)) = word;
169 
170           /* Skip redundant whitespaces */
171           while (*dup_opts == ' ' ||
172                  *dup_opts == '\t') {
173             pr_signals_handle();
174             dup_opts++;
175           }
176         }
177 
178       } else {
179         *((char **) push_array(filetab_clients_list)) =
180           pstrdup(filetab->tab_pool, res);
181       }
182 
183     } else {
184       wrap2_log("file '%s': skipping irrevelant daemon/service ('%s') line %u",
185         filetab->tab_name, service, lineno);
186     }
187   }
188 
189   return;
190 }
191 
filetab_close_cb(wrap2_table_t * filetab)192 static int filetab_close_cb(wrap2_table_t *filetab) {
193   int res = pr_fsio_close((pr_fh_t *) filetab->tab_handle);
194   filetab->tab_handle = NULL;
195 
196   filetab_clients_list = NULL;
197   filetab_daemons_list = NULL;
198   filetab_options_list = NULL;
199 
200   filetab_service_name = NULL;
201 
202   return res;
203 }
204 
filetab_fetch_clients_cb(wrap2_table_t * filetab,const char * name)205 static array_header *filetab_fetch_clients_cb(wrap2_table_t *filetab,
206     const char *name) {
207 
208   /* If this table/file has not yet been parsed, parse it. */
209   if (*((unsigned char *) filetab->tab_data) == FALSE) {
210     filetab_parse_table(filetab);
211     *((unsigned char *) filetab->tab_data) = TRUE;
212   }
213 
214   return filetab_clients_list;
215 }
216 
filetab_fetch_daemons_cb(wrap2_table_t * filetab,const char * name)217 static array_header *filetab_fetch_daemons_cb(wrap2_table_t *filetab,
218     const char *name) {
219 
220   filetab_service_name = name;
221 
222   /* If this table/file has not yet been parsed, parse it. */
223   if (*((unsigned char *) filetab->tab_data) == FALSE) {
224     filetab_parse_table(filetab);
225     *((unsigned char *) filetab->tab_data) = TRUE;
226   }
227 
228   return filetab_daemons_list;
229 }
230 
filetab_fetch_options_cb(wrap2_table_t * filetab,const char * name)231 static array_header *filetab_fetch_options_cb(wrap2_table_t *filetab,
232     const char *name) {
233 
234   /* If this table/file has not yet been parsed, parse it. */
235   if (*((unsigned char *) filetab->tab_data) == FALSE) {
236     filetab_parse_table(filetab);
237     *((unsigned char *) filetab->tab_data) = TRUE;
238   }
239 
240   return filetab_options_list;
241 }
242 
filetab_open_cb(pool * parent_pool,const char * srcinfo)243 static wrap2_table_t *filetab_open_cb(pool *parent_pool, const char *srcinfo) {
244   struct stat st;
245   wrap2_table_t *tab = NULL;
246   pool *tab_pool = make_sub_pool(parent_pool);
247 
248   /* Do not allow relative paths. */
249   if (*srcinfo != '/' &&
250       *srcinfo != '~') {
251     wrap2_log("error: table relative paths are forbidden: '%s'", srcinfo);
252     destroy_pool(tab_pool);
253     errno = EINVAL;
254     return NULL;
255   }
256 
257   /* If the path starts with a tilde, expand it out. */
258   if (srcinfo[0] == '~' &&
259       srcinfo[1] == '/') {
260     char *path = NULL;
261 
262     PRIVS_USER
263     path = dir_realpath(tab_pool, srcinfo);
264     PRIVS_RELINQUISH
265 
266     if (path) {
267       srcinfo = path;
268       wrap2_log("resolved tilde: path now '%s'", srcinfo);
269     }
270   }
271 
272   /* If the path contains a %U variable, interpolate it. */
273   if (strstr(srcinfo, "%U") != NULL) {
274     const char *orig_user;
275 
276     orig_user = pr_table_get(session.notes, "mod_auth.orig-user", NULL);
277     if (orig_user != NULL) {
278       const char *interp_path;
279 
280       interp_path = sreplace(tab_pool, srcinfo, "%U", orig_user, NULL);
281       if (interp_path != NULL) {
282         srcinfo = interp_path;
283         wrap2_log("resolved %%U: path now '%s'", srcinfo);
284       }
285     }
286   }
287 
288   tab = (wrap2_table_t *) pcalloc(tab_pool, sizeof(wrap2_table_t));
289   tab->tab_pool = tab_pool;
290 
291   /* Open the table handle */
292   while ((tab->tab_handle = (void *) pr_fsio_open(srcinfo, O_RDONLY)) == NULL) {
293     int xerrno = errno;
294 
295     if (xerrno == EINTR) {
296       pr_signals_handle();
297       continue;
298     }
299 
300     destroy_pool(tab->tab_pool);
301     errno = xerrno;
302     return NULL;
303   }
304 
305   /* Stat the opened file to determine the optimal buffer size for IO. */
306   memset(&st, 0, sizeof(st));
307   if (pr_fsio_fstat((pr_fh_t *) tab->tab_handle, &st) < 0) {
308     int xerrno = errno;
309 
310     destroy_pool(tab->tab_pool);
311     pr_fsio_close((pr_fh_t *) tab->tab_handle);
312     tab->tab_handle = NULL;
313 
314     errno = xerrno;
315     return NULL;
316   }
317 
318   if (S_ISDIR(st.st_mode)) {
319     int xerrno = EISDIR;
320 
321     destroy_pool(tab->tab_pool);
322     pr_fsio_close((pr_fh_t *) tab->tab_handle);
323     tab->tab_handle = NULL;
324 
325     errno = xerrno;
326     return NULL;
327   }
328 
329   ((pr_fh_t *) tab->tab_handle)->fh_iosz = st.st_blksize;
330 
331   tab->tab_name = pstrdup(tab->tab_pool, srcinfo);
332 
333   /* Set the necessary callbacks. */
334   tab->tab_close = filetab_close_cb;
335   tab->tab_fetch_clients = filetab_fetch_clients_cb;
336   tab->tab_fetch_daemons = filetab_fetch_daemons_cb;
337   tab->tab_fetch_options = filetab_fetch_options_cb;
338 
339   /* Use the tab_data member as a Boolean flag. */
340   tab->tab_data = pcalloc(tab->tab_pool, sizeof(unsigned char));
341   *((unsigned char *) tab->tab_data) = FALSE;
342 
343   return tab;
344 }
345 
346 /* Event handlers
347  */
348 
349 #if defined(PR_SHARED_MODULE)
filetab_mod_unload_ev(const void * event_data,void * user_data)350 static void filetab_mod_unload_ev(const void *event_data, void *user_data) {
351   if (strcmp("mod_wrap2_file.c", (const char *) event_data) == 0) {
352     pr_event_unregister(&wrap2_file_module, NULL, NULL);
353     wrap2_unregister("file");
354   }
355 }
356 #endif /* PR_SHARED_MODULE */
357 
358 /* Initialization routines
359  */
360 
filetab_init(void)361 static int filetab_init(void) {
362 
363   /* Initialize the wrap source objects for type "file". */
364   wrap2_register("file", filetab_open_cb);
365 
366 #if defined(PR_SHARED_MODULE)
367   pr_event_register(&wrap2_file_module, "core.module-unload",
368     filetab_mod_unload_ev, NULL);
369 #endif /* PR_SHARED_MODULE */
370 
371   return 0;
372 }
373 
374 module wrap2_file_module = {
375   NULL, NULL,
376 
377   /* Module API version 2.0 */
378   0x20,
379 
380   /* Module name */
381   "wrap2_file",
382 
383   /* Module configuration handler table */
384   NULL,
385 
386   /* Module command handler table */
387   NULL,
388 
389   /* Module authentication handler table */
390   NULL,
391 
392   /* Module initialization function */
393   filetab_init,
394 
395   /* Session initialization function */
396   NULL,
397 
398   /* Module version */
399   MOD_WRAP2_FILE_VERSION
400 };
401