aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--CMakeLists.txt13
-rw-r--r--multihome.c501
-rw-r--r--multihome.h25
4 files changed, 541 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 46f42f8..8ea0e40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+.idea/
+cmake-build-debug/
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..368e6ce
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,13 @@
+cmake_minimum_required(VERSION 3.17)
+project(multihome C)
+
+set(CMAKE_C_STANDARD 99)
+
+include_directories(.)
+
+add_executable(multihome
+ multihome.c
+ multihome.h)
+
+install(TARGETS multihome
+ RUNTIME DESTINATION bin) \ No newline at end of file
diff --git a/multihome.c b/multihome.c
new file mode 100644
index 0000000..80234fe
--- /dev/null
+++ b/multihome.c
@@ -0,0 +1,501 @@
+#include <wait.h>
+#include <argp.h>
+#include <time.h>
+#include "multihome.h"
+
+/**
+ * Globals
+ */
+struct {
+ char path_new[PATH_MAX];
+ char path_old[PATH_MAX];
+ char path_topdir[PATH_MAX];
+ char marker[PATH_MAX];
+ char entry_point[PATH_MAX];
+ char config_dir[PATH_MAX];
+ char config_transfer[PATH_MAX];
+ char config_skeleton[PATH_MAX];
+ char config_init[PATH_MAX];
+} multihome;
+
+/**
+ * Generic function to free an array of pointers
+ * @param arr an array
+ * @param nelem if nelem is 0 free until NULL. >0 free until nelem
+ */
+void free_array(void **arr, size_t nelem) {
+ if (nelem) {
+ for (size_t i = 0; i < nelem; i++) {
+ free(arr[i]);
+ }
+ } else {
+ for (size_t i = 0; arr[i] != NULL; i++) {
+ free(arr[i]);
+ }
+ }
+ free(arr);
+ arr = NULL;
+}
+
+/**
+ * Return the count of a substring in a string
+ * @param s Input string
+ * @param sub Input substring
+ * @return count
+ */
+ssize_t count_substrings(const char *s, char *sub) {
+ char *str;
+ char *str_orig;
+ size_t str_length;
+ size_t sub_length;
+ size_t result;
+
+ str = strdup(s);
+ if (str == NULL) {
+ return -1;
+ }
+
+ str_orig = str;
+ str_length = strlen(str);
+ sub_length = strlen(sub);
+ result = 0;
+
+ for (size_t i = 0; i < str_length; i++) {
+ char *ptr;
+ ptr = strstr(str, sub);
+
+ if (ptr) {
+ result++;
+ } else {
+ break;
+ }
+
+ if (i < str_length - sub_length) {
+ str = ptr + sub_length;
+ }
+ }
+
+ free(str_orig);
+ return result;
+}
+
+/**
+ * Split a string using a substring
+ * @param sptr Input string
+ * @param delim Substring to split on
+ * @param num_alloc Address to store count of allocated records
+ * @return NULL terminated array of strings
+ */
+char **split(const char *sptr, char *delim, size_t *num_alloc) {
+ char *s;
+ char *s_orig;
+ char **result;
+ char *token;
+
+ token = NULL;
+ result = NULL;
+ s = strdup(sptr);
+ s_orig = s;
+ if (s == NULL) {
+ return NULL;
+ }
+
+ *num_alloc = count_substrings(s, delim);
+ if (*num_alloc < 0) {
+ goto split_die_2;
+ }
+
+ *num_alloc += 2;
+ result = calloc(*num_alloc, sizeof(char *));
+
+ if (result == NULL) {
+ goto split_die_1;
+ }
+
+ for (size_t i = 0; (token = strsep(&s, delim)) != NULL; i++) {
+ result[i] = strdup(token);
+ if (result[i] == NULL) {
+ break;
+ }
+ }
+
+split_die_1:
+ free(s_orig);
+split_die_2:
+ return result;
+}
+
+/**
+ * Create directories if they do not exist
+ * @param path Filesystem path
+ * @return int (0=success, -1=error (errno set))
+ */
+int mkdirs(char *path) {
+ char **parts;
+ char tmp[PATH_MAX];
+ size_t parts_length;
+ memset(tmp, '\0', sizeof(tmp));
+
+ parts = split(path, "/", &parts_length);
+
+ for (size_t i = 0; parts[i] != NULL; i++) {
+ if (i == 0 && strlen(parts[i]) == 0) {
+ continue;
+ }
+ strcat(tmp, "/");
+ strcat(tmp, parts[i]);
+
+ if (access(tmp, F_OK) == 0) {
+ continue;
+ }
+
+ if (mkdir(tmp, (mode_t) 0755) < 0) {
+ perror("mkdir");
+ return -1;
+ }
+ }
+
+ free_array((void **)parts, parts_length);
+ return 0;
+}
+
+/**
+ * Execute a shell program
+ * @param args (char *[]){"/path/to/program", "arg1", "arg2, ..., NULL};
+ * @return exit code of program
+ */
+int shell(char *args[]){
+ pid_t pid;
+ int status;
+ int child_status;
+
+ status = 0;
+ child_status = 0;
+ pid = fork();
+ if (pid == 0) {
+ execvp(args[0], &args[0]);
+ } else if (pid < 0) {
+ fprintf(stderr, "failed to execute");
+ exit(1);
+ } else {
+ if ((waitpid(WAIT_MYPGRP, &child_status, 0)) < 0) {
+ perror("wait failed");
+ exit(1);
+ }
+
+ if (WIFEXITED(child_status) || WIFSIGNALED(child_status)) {
+ status = WEXITSTATUS(child_status);
+ }
+ }
+
+ return status;
+}
+
+/**
+ * Copy files using rsync
+ * @param source file or directory
+ * @param dest file or directory
+ * @return rsync exit code
+ */
+int copy(char *source, char *dest) {
+ if (source == NULL || dest == NULL) {
+ fprintf(stderr, "copy failed. source and destination may not be NULL\n");
+ exit(1);
+ }
+
+ return shell((char *[]){RSYNC_BIN, RSYNC_ARGS, source, dest, NULL});
+}
+
+/**
+ * Create or update the modified time on a file
+ * @param filename path to file
+ * @return 0=success, -1=error (errno set)
+ */
+int touch(char *filename) {
+ FILE *fp;
+ fp = fopen(filename, "w");
+ fflush(fp);
+ if (fp == NULL) {
+ return -1;
+ }
+ fclose(fp);
+ return 0;
+}
+
+/**
+ * Generate multihome initialization script
+ */
+void write_init_script() {
+ const char *script_block = "#\n# This script was generated on %s\n#\n\n"
+ "MULTIHOME=%s\n"
+ "if [ -x $MULTIHOME ]; then\n"
+ " multihome.old=$HOME\n"
+ " # Drop environment\n"
+ " env -i\n"
+ " # Redeclare HOME\n"
+ " HOME=$($MULTIHOME)\n"
+ " if [ \"$HOME\" != \"$multihome.old\" ]; then\n"
+ " cd $HOME\n"
+ " fi\n"
+ "fi\n";
+ char buf[PATH_MAX];
+ char date[100];
+ struct tm *tm;
+ time_t now;
+ FILE *fp;
+
+ if (realpath(multihome.entry_point, buf) < 0) {
+ perror(multihome.entry_point);
+ exit(errno);
+ }
+
+ fp = fopen(multihome.config_init, "w+");
+ if (fp == NULL) {
+ perror(multihome.config_init);
+ exit(errno);
+ }
+
+ time(&now);
+ tm = localtime(&now);
+ sprintf(date, "%02d-%02d-%d @ %02d:%02d:%02d",
+ tm->tm_mon, tm->tm_mday, tm->tm_year + 1900,
+ tm->tm_hour, tm->tm_min, tm->tm_sec);
+
+ fprintf(fp, script_block, date, buf);
+ fclose(fp);
+}
+
+/**
+ * Link or copy files from /home/username to /home/username/home_local/nodename
+ */
+void user_transfer() {
+ FILE *fp;
+ char config[PATH_MAX];
+ char rec[PATH_MAX];
+ size_t lineno;
+
+ sprintf(config, "%s/transfer", multihome.config_dir);
+ memset(rec, '\0', PATH_MAX);
+
+ fp = fopen(config, "r");
+ if (fp == NULL) {
+ // doesn't exist or isn't readable. non-fatal.
+ return;
+ }
+
+ // FORMAT:
+ // TYPE WHERE
+ //
+ // TYPE:
+ // L = SYMBOLIC LINK
+ // H = HARD LINK
+ // T = TRANSFER (file, directory, etc)
+ //
+ // EXAMPLE:
+ // L .Xauthority
+ // L .ssh
+ // H token.asc
+ // T special_dotfiles/
+
+ lineno = 0;
+ while (fgets(rec, PATH_MAX - 1, fp) != NULL) {
+ char *recptr;
+ char source[PATH_MAX];
+ char dest[PATH_MAX];
+
+ recptr = rec;
+
+ // Ignore: comments and inline comments
+ char *comment;
+ if (*recptr == '#') {
+ continue;
+ } else if ((comment = strstr(recptr, "#")) != NULL) {
+ comment--;
+ for (; comment != NULL && isblank(*comment) && comment > recptr; comment--) {
+ *comment = '\0';
+ }
+ }
+
+ // Ignore: bad lines without enough information
+ if (strlen(rec) < 3) {
+ fprintf(stderr, "%s:%zu: Invalid format: %s\n", config, lineno, rec);
+ continue;
+ }
+
+ recptr = &rec[2];
+
+ if (*recptr == '/') {
+ fprintf(stderr, "%s:%zu: Removing leading '/' from: %s\n", config, lineno, recptr);
+ memmove(recptr, recptr + 1, strlen(recptr) + 1);
+ }
+
+ if (recptr[strlen(recptr) - 1] == '\n') {
+ recptr[strlen(recptr) - 1] = '\0';
+ }
+
+ // construct data source path
+ sprintf(source, "%s/%s", multihome.path_old, recptr);
+
+ // construct data destination path
+ char *tmp;
+ tmp = strdup(source);
+ sprintf(dest, "%s/%s", multihome.path_new, basename(tmp));
+ free(tmp);
+
+ switch (rec[0]) {
+ case 'L':
+ if (symlink(source, dest) < 0) {
+ fprintf(stderr, "symlink: %s: %s -> %s\n", strerror(errno), source, dest);
+ }
+ break;
+ case 'H':
+ if (link(source, dest) < 0) {
+ fprintf(stderr, "hardlink: %s: %s -> %s\n", strerror(errno), source, dest);
+ }
+ break;
+ case 'T':
+ if (copy(source, dest) != 0) {
+ fprintf(stderr, "transfer: %s: %s -> %s\n", strerror(errno), source, dest);
+ }
+ break;
+ default:
+ fprintf(stderr, "%s:%zu: Invalid type: %c\n", config, lineno, rec[0]);
+ break;
+ }
+ }
+}
+
+// begin argp setup
+static char doc[] = "Partition a home directory per-host when using a centrally mounted /home";
+static char args_doc[] = "";
+static struct argp_option options[] = {
+ {"script", 's', 0, 0, "Generate runtime script"},
+ {"version", 'V', 0, 0, "Show version and exit"},
+ {0},
+};
+
+struct arguments {
+ int script;
+ int version;
+};
+
+static error_t parse_opt (int key, char *arg, struct argp_state *state) {
+ struct arguments *arguments = state->input;
+ (void) arg; // arg is not used
+
+ switch (key) {
+ case 'V':
+ arguments->version = 1;
+ break;
+ case 's':
+ arguments->script = 1;
+ break;
+ case ARGP_KEY_ARG:
+ if (state->arg_num > 1) {
+ argp_usage(state);
+ }
+ break;
+ default:
+ return ARGP_ERR_UNKNOWN;
+ }
+ return 0;
+}
+
+static struct argp argp = { options, parse_opt, args_doc, doc };
+// end of argp setup
+
+int main(int argc, char *argv[]) {
+ uid_t uid;
+ struct passwd *user_info;
+ struct utsname host_info;
+
+ struct arguments arguments;
+ arguments.script = 0;
+ arguments.version = 0;
+ argp_parse(&argp, argc, argv, 0, 0, &arguments);
+
+ if (arguments.version) {
+ puts(VERSION);
+ exit(0);
+ }
+
+ // Get account name for the effective user
+ uid = geteuid();
+ if ((user_info = getpwuid(uid)) == NULL) {
+ perror("getpwuid");
+ return errno;
+ }
+
+ // Get host information
+ if (uname(&host_info) < 0) {
+ perror("uname");
+ return errno;
+ }
+
+ // Populate multihome struct
+ strcpy(multihome.entry_point, argv[0]);
+ strcpy(multihome.path_old, getenv("HOME"));
+ sprintf(multihome.config_dir, "%s/.multihome", multihome.path_old);
+ sprintf(multihome.config_init, "%s/init", multihome.config_dir);
+ sprintf(multihome.config_skeleton, "%s/skel/", multihome.config_dir);
+ sprintf(multihome.path_new, "/home/%s/home_local/%s", user_info->pw_name, host_info.nodename);
+ sprintf(multihome.path_topdir, "%s/topdir", multihome.path_new);
+ sprintf(multihome.marker, "%s/.multihome_controlled", multihome.path_new);
+
+
+ // Create new home directory
+ if (strcmp(multihome.path_new, multihome.path_old) != 0) {
+ if (access(multihome.path_new, F_OK) < 0) {
+ fprintf(stderr, "Creating home directory: %s\n", multihome.path_new);
+ if (mkdirs(multihome.path_new) < 0) {
+ perror(multihome.path_new);
+ return errno;
+ }
+ }
+ }
+
+ // Generate symbolic link within the new home directory pointing back to the real account home directory
+ if (access(multihome.path_topdir, F_OK) != 0 ) {
+ fprintf(stderr, "Creating symlink to original home directory: %s\n", multihome.path_topdir);
+ if (symlink(multihome.path_new, multihome.path_topdir) < 0) {
+ perror(multihome.path_topdir);
+ return errno;
+ }
+ }
+
+ // Generate directory for user-defined account defaults
+ // Files placed here will be copied to the new home directory.
+ if (access(multihome.config_skeleton, F_OK) < 0) {
+ fprintf(stderr, "Creating user skel directory: %s\n", multihome.config_skeleton);
+ if (mkdirs(multihome.config_skeleton) < 0) {
+ perror(multihome.config_skeleton);
+ return errno;
+ }
+ }
+
+ if (access(multihome.marker, F_OK) < 0) {
+ // Copy system account defaults
+ fprintf(stderr, "Injecting account skeleton: %s\n", OS_SKEL_DIR);
+ copy(OS_SKEL_DIR, multihome.path_new);
+
+ // Copy user-defined account defaults
+ fprintf(stderr, "Injecting user-defined account skeleton: %s\n", multihome.config_skeleton);
+ copy(multihome.config_skeleton, multihome.path_new);
+
+ // Transfer or link user-defined files into the new home
+ fprintf(stderr, "Parsing transfer configuration, if present\n");
+ user_transfer();
+ }
+
+ // Leave our mark: "multihome was here"
+ if (access(multihome.marker, F_OK) < 0) {
+ fprintf(stderr, "Creating marker file: %s\n", multihome.marker);
+ touch(multihome.marker);
+ }
+
+ if (arguments.script) {
+ write_init_script();
+ } else {
+ printf("%s\n", multihome.path_new);
+ }
+} \ No newline at end of file
diff --git a/multihome.h b/multihome.h
new file mode 100644
index 0000000..a9b401f
--- /dev/null
+++ b/multihome.h
@@ -0,0 +1,25 @@
+//
+// Created by jhunk on 8/26/20.
+//
+
+#ifndef MULTIHOME_MULTIHOME_H
+#define MULTIHOME_MULTIHOME_H
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <limits.h>
+#include <pwd.h>
+#include <sys/utsname.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <libgen.h>
+
+#define VERSION "0.0.1"
+#define OS_SKEL_DIR "/etc/skel/" // NOTE: Trailing slash is required
+#define RSYNC_BIN "/usr/bin/rsync"
+#define RSYNC_ARGS "-aq"
+
+#endif //MULTIHOME_MULTIHOME_H