diff --git a/src/aquarium/aquarium.c b/src/aquarium/aquarium.c index 413699e..6823585 100644 --- a/src/aquarium/aquarium.c +++ b/src/aquarium/aquarium.c @@ -1,8 +1,11 @@ #include +#include "util.h" + #include #include #include #include +#include #include #include #include @@ -22,7 +25,8 @@ static void usage(void) { " %1$s [-r base] -I drive [-t template] [-k kernel_template]\n" " %1$s [-r base] -l\n" " %1$s [-r base] -s\n" - " %1$s [-r base] -T path -o template\n", + " %1$s [-r base] -T path -o template\n" + " %1$s [-r base] -y path source_file ... target_directory\n", getprogname()); exit(EXIT_FAILURE); @@ -239,6 +243,83 @@ static int do_img_out(aquarium_opts_t* opts) { return aquarium_img_out(opts, aquarium_path, out_path) < 0 ? EXIT_FAILURE : EXIT_SUCCESS; } +static char** copy_args = NULL; +static size_t copy_args_len = 0; + +static int do_copy(aquarium_opts_t* opts) { + char* const aquarium_path = aquarium_db_read_pointer_file(opts, path); + + if (!aquarium_path) { + return EXIT_FAILURE; + } + + char* const target = copy_args[--copy_args_len]; + + // make sure target directory doesn't refer to $HOME + // we have no [easy] way of getting the $HOME of the aquarium, and even if we did, what user should we assume? + // this doesn't stop shells from expanding '~', but it's better than nothing + + if (*target == '~') { + warnx("target directory '%s' refers to $HOME ('~'), which is unsupported", target); + return EXIT_FAILURE; + } + + // also make sure it doesn't contain any ".."'s, as that can be used to copy to outside of the aquarium + // XXX technically we could count these and path components to see if we really do break out of the aquarium, but that's a lot of work + + if (strstr(target, "..")) { + warnx("target directory '%s' contains '..', which is unsupported", target); + return EXIT_FAILURE; + } + + // make sure all files are (recursively) readable by the user + + for (size_t i = 0; i < copy_args_len; i++) { + char* const source = copy_args[i]; + + if (!can_read_all(source)) { + warnx("user can't read all of \"%s\"", source); + return EXIT_FAILURE; + } + } + + // once we're sure all the files are readable, setuid + // no need to set the UID back; from here on out, we stay as superuser + + if (opts->initial_uid && setuid(0) < 0) { + warnx("setuid(0): %s", strerror(errno)); + return EXIT_FAILURE; + } + + // create target directory if it doesn't yet exist + + char* abs_target; + if (asprintf(&abs_target, "%s/%s", aquarium_path, target)) {} + + if (mkdir_recursive(abs_target) < 0) { + return EXIT_FAILURE; + } + + // then, actually copy all the files recursively + + while (copy_args_len --> 0) { + char* const source = copy_args[copy_args_len]; + + if (copy_recursive(source, abs_target)) { + return EXIT_FAILURE; + } + + // recursively set UID and GID of all copied files to 0 + // this is probably not what the user wants, but better be safe than sorry - I don't see a better way for handling this ATM + + if (chown_recursive(abs_target, 0, 0) < 0) { + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} + static void parse_rulesets(aquarium_opts_t* opts, char* rulesets) { char* tok; @@ -358,6 +439,11 @@ int main(int argc, char* argv[]) { path = optarg; } + else if (c == 'y') { + action = do_copy; + path = optarg; + } + // name-passing options else if (c == 'k') { @@ -380,7 +466,12 @@ int main(int argc, char* argv[]) { argc -= optind; argv += optind; - if (argc) { + if (action == do_copy) { + copy_args = argv; + copy_args_len = argc; + } + + else if (argc) { usage(); } diff --git a/src/aquarium/util.h b/src/aquarium/util.h new file mode 100644 index 0000000..ff2df38 --- /dev/null +++ b/src/aquarium/util.h @@ -0,0 +1,304 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// TODO make these FS utility functions a library et mettre en commun avec Bob + +static void strfree(char* const* str_ref) { + char* const str = *str_ref; + + if (!str) { + return; + } + + free(str); +} + +#define CLEANUP_STR __attribute__((cleanup(strfree))) + +static bool can_read_all(char* path) { + char* const path_argv[] = { path, NULL }; + FTS* const fts = fts_open(path_argv, FTS_PHYSICAL | FTS_XDEV, NULL); + + if (fts == NULL) { + warnx("fts_open(\"%s\"): %s", path, strerror(errno)); + return false; + } + + bool accum = true; + + for (FTSENT* ent; (ent = fts_read(fts));) { + char* const path = ent->fts_path; // shadow parent scope's 'path' + + switch (ent->fts_info) { + case FTS_DP: + + break; // ignore directories being visited in postorder + + case FTS_DOT: + + warnx("fts_read: Read a '.' or '..' entry, which shouldn't happen as the 'FTS_SEEDOT' option was not passed to 'fts_open'"); + break; + + case FTS_DNR: + case FTS_ERR: + case FTS_NS: + + warnx("fts_read: Failed to read '%s': %s", path, strerror(errno)); + break; + + case FTS_SL: + case FTS_SLNONE: + case FTS_D: + case FTS_DC: + case FTS_F: + case FTS_DEFAULT: + default: + + if (access(path, R_OK) != 0) { + goto err_access; + } + } + } + + accum = true; + +err_access: + + fts_close(fts); + return accum; +} + +static int mkdir_recursive(char const* _path) { + int rv = -1; + + // we don't need to do anything if path is empty + + if (!*_path) { + return 0; + } + + char* const CLEANUP_STR orig_path = strdup(_path); + char* path = orig_path; + + // remember previous working directory, because to make our lives easier, we'll be jumping around the place to create our subdirectories + + char* const CLEANUP_STR cwd = getcwd(NULL, 0); + + if (!cwd) { + warnx("getcwd: %s", strerror(errno)); + goto err_cwd; + } + + // if we're dealing with a path relative to $HOME, chdir to $HOME first + + if (*path == '~') { + char* const home = getenv("HOME"); + + // if $HOME isn't set, treat as an absolute directory + + if (!home) { + *path = '/'; + } + + else if (chdir(home) < 0) { + warnx("chdir($HOME): %s", strerror(errno)); + goto err_home; + } + } + + // if we're dealing with an absolute path, chdir to '/' and treat path as relative + + if (*path == '/' && chdir("/") < 0) { + warnx("chdir(\"/\"): %s", strerror(errno)); + goto err_abs; + } + + // parse the path itself + + char* bit; + + while ((bit = strsep(&path, "/"))) { + // ignore if the bit is empty + + if (!bit || !*bit) { + continue; + } + + // ignore if the bit refers to the current directory + + if (!strcmp(bit, ".")) { + continue; + } + + // don't attempt to mkdir if we're going backwards, only chdir + + if (!strcmp(bit, "..")) { + goto no_mkdir; + } + + if (mkdir(bit, 0755) < 0 && errno != EEXIST) { + warnx("mkdir(\"%s\"): %s", bit, strerror(errno)); + goto err_mkdir; + } + + no_mkdir: + + if (chdir(bit) < 0) { + warnx("chdir(\"%s\"): %s", bit, strerror(errno)); + goto err_chdir; + } + } + + // success + + rv = 0; + +err_chdir: +err_mkdir: + + // move back to current directory once we're sure the output directory exists (or there's an error) + + if (chdir(cwd) < 0) { + warnx("chdir(\"%s\"): %s", cwd, strerror(errno)); + } + +err_abs: +err_home: +err_cwd: + + return rv; +} + +static int copy_recursive(char* source, char* target) { + char* const path_argv[] = { source, NULL }; + FTS* const fts = fts_open(path_argv, FTS_PHYSICAL | FTS_XDEV, NULL); + + if (fts == NULL) { + warnx("fts_open(\"%s\"): %s", source, strerror(errno)); + return -1; + } + + int rv = -1; + + for (FTSENT* ent; (ent = fts_read(fts));) { + char* const path = ent->fts_path; + char* path_end = path + strlen(source); + + if (*path_end == '\0') { + path_end = strrchr(path, '/'); + + if (path_end == NULL) { + path_end = path; + } + } + + char* CLEANUP_STR abs_path = NULL; + if (asprintf(&abs_path, "%s/%s", target, path_end)) {} + + switch (ent->fts_info) { + case FTS_DP: + + break; // ignore directories being visited in postorder + + case FTS_DOT: + + warnx("fts_read: Read a '.' or '..' entry, which shouldn't happen as the 'FTS_SEEDOT' option was not passed to 'fts_open'"); + break; + + case FTS_DNR: + case FTS_ERR: + case FTS_NS: + + warnx("fts_read: Failed to read '%s': %s", path, strerror(errno)); + break; + + case FTS_D: + case FTS_DC: {} + + if (mkdir_recursive(abs_path) < 0) { + goto err_mkdir; + } + + break; + + case FTS_SL: + case FTS_SLNONE: + case FTS_F: + case FTS_DEFAULT: + default: + + if (copyfile(path, abs_path, 0, COPYFILE_ALL) < 0) { + warnx("copyfile(\"%s\", \"%s\"): %s", path, abs_path, strerror(errno)); + goto err_copy; + } + } + } + + // success 🎉 + + rv = 0; + +err_copy: +err_mkdir: + + fts_close(fts); + return rv; +} + +static int chown_recursive(char* path, uid_t uid, gid_t gid) { + char* const path_argv[] = { path, NULL }; + FTS* const fts = fts_open(path_argv, FTS_PHYSICAL | FTS_XDEV, NULL); + + if (fts == NULL) { + warnx("fts_open(\"%s\"): %s", path, strerror(errno)); + return -1; + } + + for (FTSENT* ent; (ent = fts_read(fts));) { + char* const path = ent->fts_path; // shadow parent scope's 'path' + + switch (ent->fts_info) { + case FTS_DP: + + break; // ignore directories being visited in postorder + + case FTS_DOT: + + warnx("fts_read: Read a '.' or '..' entry, which shouldn't happen as the 'FTS_SEEDOT' option was not passed to 'fts_open'"); + break; + + case FTS_DNR: + case FTS_ERR: + case FTS_NS: + + warnx("fts_read: Failed to read '%s': %s", path, strerror(errno)); + break; + + case FTS_SL: + case FTS_SLNONE: + case FTS_D: + case FTS_DC: + case FTS_F: + case FTS_DEFAULT: + default: + + if (chown(path, uid, gid) < 0) { + warnx("chown(\"%s\", %d, %d): %s", path, uid, gid, strerror(errno)); + } + } + } + + fts_close(fts); + return 0; +} diff --git a/src/lib/os.c b/src/lib/os.c index 20129fb..ecaecb4 100644 --- a/src/lib/os.c +++ b/src/lib/os.c @@ -38,11 +38,11 @@ aquarium_os_t aquarium_os_info(char const* _path) { // match NAME with an OS we know of - if (!strstr(os, "FreeBSD")) { + if (strstr(os, "FreeBSD")) { return AQUARIUM_OS_FREEBSD; } - if (!strstr(os, "Ubuntu")) { + if (strstr(os, "Ubuntu")) { return AQUARIUM_OS_UBUNTU; }