1 /*
2 * ProFTPD: mod_vroot -- a module implementing a virtual chroot capability
3 * via the FSIO API
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
19 *
20 * As a special exemption, TJ Saunders and other respective copyright holders
21 * give permission to link this program with OpenSSL, and distribute the
22 * resulting executable, without including the source code for OpenSSL in the
23 * source distribution.
24 *
25 * This is mod_vroot, contrib software for proftpd 1.3.x and above.
26 * For more information contact TJ Saunders <tj@castaglia.org>.
27 *
28 * -----DO NOT EDIT BELOW THIS LINE-----
29 * $Archive: mod_vroot.a $
30 */
31
32 #include "mod_vroot.h"
33 #include "privs.h"
34 #include "alias.h"
35 #include "path.h"
36 #include "fsio.h"
37
38 int vroot_logfd = -1;
39 unsigned int vroot_opts = 0;
40
41 module vroot_module;
42
43 static int vroot_engine = FALSE;
44
45 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
46 static int vroot_use_mkdtemp = FALSE;
47 #endif /* ProFTPD 1.3.4c or later */
48
handle_vrootaliases(void)49 static int handle_vrootaliases(void) {
50 config_rec *c;
51 pool *tmp_pool = NULL;
52
53 /* Handle any VRootAlias settings. */
54
55 tmp_pool = make_sub_pool(session.pool);
56 pr_pool_tag(tmp_pool, "VRootAlias pool");
57
58 c = find_config(main_server->conf, CONF_PARAM, "VRootAlias", FALSE);
59 while (c != NULL) {
60 char src_path[PR_TUNABLE_PATH_MAX+1], dst_path[PR_TUNABLE_PATH_MAX+1];
61 const char *ptr;
62
63 pr_signals_handle();
64
65 /* XXX Note that by using vroot_path_lookup(), we assume a POST_CMD
66 * invocation. Looks like VRootAlias might end up being incompatible
67 * with VRootServerRoot.
68 */
69
70 memset(src_path, '\0', sizeof(src_path));
71 ptr = c->argv[0];
72
73 /* Check for any expandable variables. */
74 ptr = path_subst_uservar(tmp_pool, &ptr);
75
76 sstrncpy(src_path, ptr, sizeof(src_path)-1);
77 vroot_path_clean(src_path);
78
79 ptr = c->argv[1];
80
81 /* Check for any expandable variables. */
82 ptr = path_subst_uservar(tmp_pool, &ptr);
83
84 ptr = dir_best_path(tmp_pool, ptr);
85 vroot_path_lookup(NULL, dst_path, sizeof(dst_path)-1, ptr,
86 VROOT_LOOKUP_FL_NO_ALIAS, NULL);
87
88 if (vroot_alias_add(dst_path, src_path) < 0) {
89 /* Make a slightly better log message when there is an alias collision. */
90 if (errno == EEXIST) {
91 (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
92 "VRootAlias already configured for '%s', ignoring bad alias",
93 (char *) c->argv[1]);
94
95 } else {
96 (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
97 "error stashing VRootAlias '%s': %s", dst_path, strerror(errno));
98 }
99
100 } else {
101 (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
102 "aliased '%s' to real path '%s'", dst_path, src_path);
103 }
104
105 c = find_config_next(c, c->next, CONF_PARAM, "VRootAlias", FALSE);
106 }
107
108 destroy_pool(tmp_pool);
109 return 0;
110 }
111
112 /* Configuration handlers
113 */
114
115 /* usage: VRootAlias src-path dst-path */
set_vrootalias(cmd_rec * cmd)116 MODRET set_vrootalias(cmd_rec *cmd) {
117 config_rec *c;
118
119 CHECK_ARGS(cmd, 2);
120 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
121
122 if (pr_fs_valid_path(cmd->argv[1]) < 0) {
123 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "source path '", cmd->argv[1],
124 "' is not an absolute path", NULL));
125 }
126
127 c = add_config_param_str(cmd->argv[0], 2, cmd->argv[1], cmd->argv[2]);
128
129 /* Set this flag in order to allow mod_ifsession to work properly with
130 * multiple VRootAlias directives.
131 */
132 c->flags |= CF_MERGEDOWN_MULTI;
133
134 return PR_HANDLED(cmd);
135 }
136
137 /* usage: VRootEngine on|off */
set_vrootengine(cmd_rec * cmd)138 MODRET set_vrootengine(cmd_rec *cmd) {
139 int engine = -1;
140 config_rec *c = NULL;
141
142 CHECK_ARGS(cmd, 1);
143 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
144
145 engine = get_boolean(cmd, 1);
146 if (engine == -1) {
147 CONF_ERROR(cmd, "expected Boolean parameter");
148 }
149
150 c = add_config_param(cmd->argv[0], 1, NULL);
151 c->argv[0] = pcalloc(c->pool, sizeof(int));
152 *((int *) c->argv[0]) = engine;
153
154 return PR_HANDLED(cmd);
155 }
156
157 /* usage: VRootLog path|"none" */
set_vrootlog(cmd_rec * cmd)158 MODRET set_vrootlog(cmd_rec *cmd) {
159 CHECK_ARGS(cmd, 1);
160 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
161
162 if (pr_fs_valid_path(cmd->argv[1]) < 0) {
163 CONF_ERROR(cmd, "must be an absolute path");
164 }
165
166 (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
167 return PR_HANDLED(cmd);
168 }
169
170 /* usage: VRootOptions opt1 opt2 ... optN */
set_vrootoptions(cmd_rec * cmd)171 MODRET set_vrootoptions(cmd_rec *cmd) {
172 config_rec *c = NULL;
173 register unsigned int i;
174 unsigned int opts = 0U;
175
176 if (cmd->argc-1 == 0) {
177 CONF_ERROR(cmd, "wrong number of parameters");
178 }
179
180 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
181
182 c = add_config_param(cmd->argv[0], 1, NULL);
183 for (i = 1; i < cmd->argc; i++) {
184 if (strcasecmp(cmd->argv[i], "AllowSymlinks") == 0) {
185 opts |= VROOT_OPT_ALLOW_SYMLINKS;
186
187 } else {
188 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown VRootOption: '",
189 cmd->argv[i], "'", NULL));
190 }
191 }
192
193 c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
194 *((unsigned int *) c->argv[0]) = opts;
195
196 return PR_HANDLED(cmd);
197 }
198
199 /* usage: VRootServerRoot path */
set_vrootserverroot(cmd_rec * cmd)200 MODRET set_vrootserverroot(cmd_rec *cmd) {
201 struct stat st;
202 config_rec *c;
203 char *path;
204 size_t pathlen;
205
206 CHECK_ARGS(cmd, 1);
207 CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
208
209 path = cmd->argv[1];
210
211 if (pr_fs_valid_path(path) < 0) {
212 CONF_ERROR(cmd, "must be an absolute path");
213 }
214
215 if (stat(path, &st) < 0) {
216 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", path, "': ",
217 strerror(errno), NULL));
218 }
219
220 if (!S_ISDIR(st.st_mode)) {
221 CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' is not a directory",
222 NULL));
223 }
224
225 c = add_config_param(cmd->argv[0], 1, NULL);
226
227 /* Make sure the configured path has a trailing path separater ('/').
228 * This is important.
229 */
230
231 pathlen = strlen(path);
232 if (path[pathlen - 1] != '/') {
233 c->argv[0] = pstrcat(c->pool, path, "/", NULL);
234
235 } else {
236 c->argv[0] = pstrdup(c->pool, path);
237 }
238
239 return PR_HANDLED(cmd);
240 }
241
242 /* Command handlers
243 */
244
vroot_log_retr(cmd_rec * cmd)245 MODRET vroot_log_retr(cmd_rec *cmd) {
246 const char *key, *path;
247
248 if (vroot_engine == FALSE ||
249 session.chroot_path == NULL) {
250 return PR_DECLINED(cmd);
251 }
252
253 key = "mod_xfer.retr-path";
254
255 path = pr_table_get(cmd->notes, key, NULL);
256 if (path != NULL) {
257 char *real_path;
258
259 if (*path == '/') {
260 const char *base_path;
261
262 base_path = vroot_path_get_base(cmd->tmp_pool, NULL);
263 real_path = pdircat(cmd->pool, base_path, path, NULL);
264 vroot_path_clean(real_path);
265
266 } else {
267 real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH);
268 }
269
270 pr_table_set(cmd->notes, key, real_path, 0);
271 }
272
273 return PR_DECLINED(cmd);
274 }
275
vroot_log_stor(cmd_rec * cmd)276 MODRET vroot_log_stor(cmd_rec *cmd) {
277 const char *key, *path;
278
279 if (vroot_engine == FALSE ||
280 session.chroot_path == NULL) {
281 return PR_DECLINED(cmd);
282 }
283
284 key = "mod_xfer.store-path";
285
286 path = pr_table_get(cmd->notes, key, NULL);
287 if (path != NULL) {
288 char *real_path;
289
290 if (*path == '/') {
291 const char *base_path;
292
293 base_path = vroot_path_get_base(cmd->tmp_pool, NULL);
294 real_path = pdircat(cmd->pool, base_path, path, NULL);
295 vroot_path_clean(real_path);
296
297 } else {
298 real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH);
299 }
300
301 pr_table_set(cmd->notes, key, real_path, 0);
302 }
303
304 return PR_DECLINED(cmd);
305 }
306
vroot_pre_mkd(cmd_rec * cmd)307 MODRET vroot_pre_mkd(cmd_rec *cmd) {
308 if (vroot_engine == FALSE ||
309 session.chroot_path == NULL) {
310 return PR_DECLINED(cmd);
311 }
312
313 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
314 vroot_use_mkdtemp = pr_fsio_set_use_mkdtemp(FALSE);
315 #endif /* ProFTPD 1.3.4c or later */
316
317 return PR_DECLINED(cmd);
318 }
319
vroot_post_mkd(cmd_rec * cmd)320 MODRET vroot_post_mkd(cmd_rec *cmd) {
321 if (vroot_engine == FALSE ||
322 session.chroot_path == NULL) {
323 return PR_DECLINED(cmd);
324 }
325
326 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
327 pr_fsio_set_use_mkdtemp(vroot_use_mkdtemp);
328 #endif /* ProFTPD 1.3.4c or later */
329
330 return PR_DECLINED(cmd);
331 }
332
vroot_pre_pass(cmd_rec * cmd)333 MODRET vroot_pre_pass(cmd_rec *cmd) {
334 pr_fs_t *fs = NULL;
335 int *use_vroot = NULL;
336
337 use_vroot = get_param_ptr(main_server->conf, "VRootEngine", FALSE);
338 if (use_vroot == NULL ||
339 *use_vroot == FALSE) {
340 vroot_engine = FALSE;
341 return PR_DECLINED(cmd);
342 }
343
344 /* First, make sure that we have not already registered our FS object. */
345 fs = pr_unmount_fs("/", "vroot");
346 if (fs != NULL) {
347 destroy_pool(fs->fs_pool);
348 }
349
350 fs = pr_register_fs(main_server->pool, "vroot", "/");
351 if (fs == NULL) {
352 pr_log_debug(DEBUG3, MOD_VROOT_VERSION ": error registering fs: %s",
353 strerror(errno));
354 return PR_DECLINED(cmd);
355 }
356
357 pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot registered");
358
359 /* Add the module's custom FS callbacks here. This module does not
360 * provide callbacks for the following (as they are unnecessary):
361 * close(), read(), write(), and lseek().
362 */
363 fs->stat = vroot_fsio_stat;
364 fs->lstat = vroot_fsio_lstat;
365 fs->rename = vroot_fsio_rename;
366 fs->unlink = vroot_fsio_unlink;
367 fs->open = vroot_fsio_open;
368 #if PROFTPD_VERSION_NUMBER < 0x0001030603
369 fs->creat = vroot_fsio_creat;
370 #endif /* ProFTPD 1.3.6rc2 or earlier */
371 fs->link = vroot_fsio_link;
372 fs->readlink = vroot_fsio_readlink;
373 fs->symlink = vroot_fsio_symlink;
374 fs->truncate = vroot_fsio_truncate;
375 fs->chmod = vroot_fsio_chmod;
376 fs->chown = vroot_fsio_chown;
377 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
378 fs->lchown = vroot_fsio_lchown;
379 #endif /* ProFTPD 1.3.4c or later */
380 fs->chdir = vroot_fsio_chdir;
381 fs->chroot = vroot_fsio_chroot;
382 fs->utimes = vroot_fsio_utimes;
383 fs->opendir = vroot_fsio_opendir;
384 fs->readdir = vroot_fsio_readdir;
385 fs->closedir = vroot_fsio_closedir;
386 fs->mkdir = vroot_fsio_mkdir;
387 fs->rmdir = vroot_fsio_rmdir;
388
389 vroot_engine = TRUE;
390 return PR_DECLINED(cmd);
391 }
392
vroot_post_pass(cmd_rec * cmd)393 MODRET vroot_post_pass(cmd_rec *cmd) {
394 if (vroot_engine == TRUE) {
395
396 /* If not chrooted, unregister vroot. */
397 if (session.chroot_path == NULL) {
398 if (pr_unregister_fs("/") < 0) {
399 pr_log_debug(DEBUG2, MOD_VROOT_VERSION
400 ": error unregistering vroot: %s", strerror(errno));
401
402 } else {
403 pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered");
404 pr_fs_setcwd(pr_fs_getvwd());
405 pr_fs_clear_cache();
406 }
407
408 } else {
409 config_rec *c;
410
411 /* Otherwise, lookup and process any VRootOptions. */
412 c = find_config(main_server->conf, CONF_PARAM, "VRootOptions", FALSE);
413 if (c != NULL) {
414 vroot_opts = *((unsigned int *) c->argv[0]);
415 }
416
417 /* XXX This needs to be in the PRE_CMD PASS handler, as when
418 * VRootServer is used, so that a real chroot(2) occurs.
419 */
420 handle_vrootaliases();
421 }
422 }
423
424 return PR_DECLINED(cmd);
425 }
426
vroot_post_pass_err(cmd_rec * cmd)427 MODRET vroot_post_pass_err(cmd_rec *cmd) {
428 if (vroot_engine == TRUE) {
429 /* If not chrooted, unregister vroot. */
430 if (session.chroot_path == NULL) {
431 if (pr_unregister_fs("/") < 0) {
432 pr_log_debug(DEBUG2, MOD_VROOT_VERSION
433 ": error unregistering vroot: %s", strerror(errno));
434
435 } else {
436 pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered");
437 }
438 }
439
440 vroot_alias_free();
441 }
442
443 return PR_DECLINED(cmd);
444 }
445
446 /* Event listeners
447 */
448
vroot_exit_ev(const void * event_data,void * user_data)449 static void vroot_exit_ev(const void *event_data, void *user_data) {
450 vroot_alias_free();
451 vroot_fsio_free();
452 }
453
454 /* Initialization routines
455 */
456
vroot_sess_init(void)457 static int vroot_sess_init(void) {
458 config_rec *c;
459
460 c = find_config(main_server->conf, CONF_PARAM, "VRootLog", FALSE);
461 if (c != NULL) {
462 const char *path;
463
464 path = c->argv[0];
465 if (strcasecmp(path, "none") != 0) {
466 int res, xerrno;
467
468 PRIVS_ROOT
469 res = pr_log_openfile(path, &vroot_logfd, 0660);
470 xerrno = errno;
471 PRIVS_RELINQUISH
472
473 switch (res) {
474 case 0:
475 break;
476
477 case -1:
478 pr_log_debug(DEBUG1, MOD_VROOT_VERSION
479 ": unable to open VRootLog '%s': %s", path, strerror(xerrno));
480 break;
481
482 case PR_LOG_SYMLINK:
483 pr_log_debug(DEBUG1, MOD_VROOT_VERSION
484 ": unable to open VRootLog '%s': %s", path, "is a symlink");
485 break;
486
487 case PR_LOG_WRITABLE_DIR:
488 pr_log_debug(DEBUG1, MOD_VROOT_VERSION
489 ": unable to open VRootLog '%s': %s", path,
490 "parent directory is world-writable");
491 break;
492 }
493 }
494 }
495
496 vroot_alias_init(session.pool);
497 vroot_fsio_init(session.pool);
498 pr_event_register(&vroot_module, "core.exit", vroot_exit_ev, NULL);
499
500 return 0;
501 }
502
503 /* Module API tables
504 */
505
506 static conftable vroot_conftab[] = {
507 { "VRootAlias", set_vrootalias, NULL },
508 { "VRootEngine", set_vrootengine, NULL },
509 { "VRootLog", set_vrootlog, NULL },
510 { "VRootOptions", set_vrootoptions, NULL },
511 { "VRootServerRoot", set_vrootserverroot, NULL },
512 { NULL }
513 };
514
515 static cmdtable vroot_cmdtab[] = {
516 { PRE_CMD, C_PASS, G_NONE, vroot_pre_pass, FALSE, FALSE },
517 { POST_CMD, C_PASS, G_NONE, vroot_post_pass, FALSE, FALSE },
518 { POST_CMD_ERR, C_PASS, G_NONE, vroot_post_pass_err, FALSE, FALSE },
519
520 { PRE_CMD, C_MKD, G_NONE, vroot_pre_mkd, FALSE, FALSE },
521 { POST_CMD, C_MKD, G_NONE, vroot_post_mkd, FALSE, FALSE },
522 { POST_CMD_ERR, C_MKD, G_NONE, vroot_post_mkd, FALSE, FALSE },
523 { PRE_CMD, C_XMKD, G_NONE, vroot_pre_mkd, FALSE, FALSE },
524 { POST_CMD, C_XMKD, G_NONE, vroot_post_mkd, FALSE, FALSE },
525 { POST_CMD_ERR, C_XMKD, G_NONE, vroot_post_mkd, FALSE, FALSE },
526
527 /* These command handlers are for manipulating cmd->notes, to get
528 * paths properly logged.
529 *
530 * Ideally these would be LOG_CMD/LOG_CMD_ERR phase handlers. HOWEVER,
531 * we need to transform things before the cmd is dispatched to mod_log,
532 * and mod_log uses a C_ANY handler for logging. And when dispatching,
533 * C_ANY handlers are run before named handlers. This means that using
534 * LOG_CMD/LOG_CMD_ERR handlers would be run AFTER mod_log's handler,
535 * even though we appear BEFORE mod_log in the module load order.
536 *
537 * Thus to do the transformation, we actually use CMD/POST_CMD_ERR phase
538 * handlers here.
539 */
540 { CMD, C_APPE, G_NONE, vroot_log_stor, FALSE, FALSE },
541 { POST_CMD_ERR, C_APPE, G_NONE, vroot_log_stor, FALSE, FALSE },
542 { CMD, C_RETR, G_NONE, vroot_log_retr, FALSE, FALSE },
543 { POST_CMD_ERR, C_RETR, G_NONE, vroot_log_retr, FALSE, FALSE },
544 { CMD, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE },
545 { POST_CMD_ERR, C_STOR, G_NONE, vroot_log_stor, FALSE, FALSE },
546
547 { 0, NULL }
548 };
549
550 module vroot_module = {
551 NULL, NULL,
552
553 /* Module API version 2.0 */
554 0x20,
555
556 /* Module name */
557 "vroot",
558
559 /* Module configuration handler table */
560 vroot_conftab,
561
562 /* Module command handler table */
563 vroot_cmdtab,
564
565 /* Module authentication handler table */
566 NULL,
567
568 /* Module initialization function */
569 NULL,
570
571 /* Session initialization function */
572 vroot_sess_init,
573
574 /* Module version */
575 MOD_VROOT_VERSION
576 };
577