/* sandbox.c -- OS-level sandboxing for Hiawatha
 *
 * Defense-in-depth philosophy: sandbox errors are never fatal.
 * If a syscall fails (old kernel, missing support), we log and continue.
 * The application always runs. Sandboxing is an additional protection layer.
 *
 * Platforms:
 *   OpenBSD: pledge(2) + unveil(2)
 *   Linux:   Landlock + seccomp-bpf
 *   macOS:   Seatbelt (sandbox-exec re-exec)
 *   Other:   No-op with log warning
 */

#include "config.h"

#ifdef ENABLE_SANDBOX

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include "libstr.h"
#include "sandbox.h"
#include "serverconfig.h"
#include "memdbg.h"

// Maximum number of paths we track for sandboxing
#define SANDBOX_MAX_PATHS 128

typedef struct {
	const char *path;
	const char *perms; // "r", "rw", "rwc", "rx"
} t_sandbox_path;

static t_sandbox_path sandbox_paths[SANDBOX_MAX_PATHS];
static int sandbox_path_count = 0;

static void sandbox_add_path(const char *path, const char *perms) {
	if ((path == NULL) || (perms == NULL)) {
		return;
	}

	if (sandbox_path_count >= SANDBOX_MAX_PATHS) {
		fprintf(stderr, "Sandbox: too many paths (max %d), skipping %s\n", SANDBOX_MAX_PATHS, path);
		return;
	}

	sandbox_paths[sandbox_path_count].path = path;
	sandbox_paths[sandbox_path_count].perms = perms;
	sandbox_path_count++;
}

int sandbox_collect_paths(t_config *config) {
	t_host *host;
	bool cgi_enabled = false;

	sandbox_path_count = 0;

	// Global paths
	sandbox_add_path(config->system_logfile, "rw");
	sandbox_add_path(config->garbage_logfile, "rw");
	sandbox_add_path(config->exploit_logfile, "rw");
	sandbox_add_path(config->work_directory, "rwc");
	sandbox_add_path(config->upload_directory, "rwc");
	sandbox_add_path(config->gzipped_directory, "rwc");
	sandbox_add_path(config->pidfile, "rw");

	// Per-host paths
	host = config->first_host;
	while (host != NULL) {
		sandbox_add_path(host->website_root, host->execute_cgi ? "rx" : "r");
		sandbox_add_path(host->access_logfile, "rw");
		sandbox_add_path(host->error_logfile, "rw");
		if (host->execute_cgi) {
			cgi_enabled = true;
		}
		host = host->next;
	}

	// System paths needed at runtime
	sandbox_add_path("/etc/ssl/certs", "r");
	sandbox_add_path("/dev/urandom", "r");
	sandbox_add_path("/dev/null", "rw");

#ifdef __OpenBSD__
    /* OpenBSD 7.9+: pledge no longer grants implicit open() access
     * to special paths — they must be explicitly unveiled.
     */
    sandbox_add_path("/etc/localtime", "r");
    sandbox_add_path("/usr/share/zoneinfo", "r");
    sandbox_add_path("/etc/resolv.conf", "r");
    sandbox_add_path("/etc/hosts", "r");
    sandbox_add_path("/etc/services", "r");
    sandbox_add_path("/etc/protocols", "r");
#endif

#ifdef __linux__
	if (cgi_enabled) {
		sandbox_add_path("/usr/lib", "rx");
		sandbox_add_path("/usr/bin", "rx");
		sandbox_add_path("/lib", "rx");
		sandbox_add_path("/bin", "rx");
	} else {
		sandbox_add_path("/usr/lib", "r");
		sandbox_add_path("/lib", "r");
	}
	sandbox_add_path("/etc/ld.so.cache", "r");
#endif

	return 0;
}

void sandbox_print_paths(void) {
	int i;

	printf("sandbox:");
#if defined(__OpenBSD__)
	printf(" pledge/unveil\n");
	printf("  promises: stdio rpath wpath cpath fattr inet dns proc\n");
	for (i = 0; i < sandbox_path_count; i++) {
		printf("  unveil: %s (%s)\n", sandbox_paths[i].path, sandbox_paths[i].perms);
	}
#elif defined(__linux__)
	printf(" landlock/seccomp\n");
	for (i = 0; i < sandbox_path_count; i++) {
		printf("  path: %s (%s)\n", sandbox_paths[i].path, sandbox_paths[i].perms);
	}
#elif defined(__APPLE__)
	printf(" seatbelt\n");
	for (i = 0; i < sandbox_path_count; i++) {
		printf("  path: %s (%s)\n", sandbox_paths[i].path, sandbox_paths[i].perms);
	}
#else
	printf(" none\n");
#endif
}

/* =========================================================================
 * OpenBSD: pledge(2) + unveil(2)
 * =========================================================================
 */
#if defined(__OpenBSD__)

#include <unistd.h>

int sandbox_apply(void) {
	int i;

	for (i = 0; i < sandbox_path_count; i++) {
		if (unveil(sandbox_paths[i].path, sandbox_paths[i].perms) == -1) {
			fprintf(stderr, "Sandbox: unveil(%s) failed, continuing.\n",
			        sandbox_paths[i].path);
		}
	}

	if (unveil(NULL, NULL) == -1) {
		fprintf(stderr, "Sandbox: unveil lock failed, continuing.\n");
	}

	if (pledge("stdio rpath wpath cpath fattr inet dns proc", NULL) == -1) {
		fprintf(stderr, "Sandbox: pledge failed, continuing.\n");
		return -1;
	}

	fprintf(stdout, "Sandbox: OpenBSD pledge/unveil applied.\n");
	return 0;
}

/* =========================================================================
 * Linux: Landlock + seccomp-bpf
 * =========================================================================
 */
#elif defined(__linux__)

#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <linux/landlock.h>

#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>

#ifndef LANDLOCK_CREATE_RULESET_VERSION
#define LANDLOCK_CREATE_RULESET_VERSION (1 << 0)
#endif

static int landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
                                    size_t size, __u32 flags) {
	return (int)syscall(__NR_landlock_create_ruleset, attr, size, flags);
}

static int landlock_add_rule(int ruleset_fd,
                              enum landlock_rule_type rule_type,
                              const void *rule_attr, __u32 flags) {
	return (int)syscall(__NR_landlock_add_rule, ruleset_fd, rule_type,
	                    rule_attr, flags);
}

static int landlock_restrict_self(int ruleset_fd, __u32 flags) {
	return (int)syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}

static __u64 perms_to_landlock_dir(const char *perms) {
	__u64 access = 0;

	if (perms == NULL) {
		return 0;
	}

	if (strchr(perms, 'r') != NULL) {
		access |= LANDLOCK_ACCESS_FS_READ_FILE |
		           LANDLOCK_ACCESS_FS_READ_DIR;
	}
	if (strchr(perms, 'w') != NULL) {
		access |= LANDLOCK_ACCESS_FS_WRITE_FILE |
		           LANDLOCK_ACCESS_FS_REMOVE_FILE |
		           LANDLOCK_ACCESS_FS_REMOVE_DIR;
	}
	if (strchr(perms, 'c') != NULL) {
		access |= LANDLOCK_ACCESS_FS_MAKE_CHAR |
		           LANDLOCK_ACCESS_FS_MAKE_DIR |
		           LANDLOCK_ACCESS_FS_MAKE_REG;
	}
	if (strchr(perms, 'x') != NULL) {
		access |= LANDLOCK_ACCESS_FS_EXECUTE;
	}

	return access;
}


static int add_landlock_path(int ruleset_fd, const char *path, const char *perms) {
	struct stat st;
	int fd;
	__u64 access;
	struct landlock_path_beneath_attr path_beneath = {0};
	char parent[4096];
	const char *open_path;
	char *slash;

	if (stat(path, &st) == 0) {
		if (S_ISDIR(st.st_mode)) {
			open_path = path;
			access = perms_to_landlock_dir(perms);
		} else {
			/* Regular file or device — open parent directory instead.
			 * Landlock RULE_PATH_BENEATH needs a directory fd;
			 * individual files (including device nodes like /dev/urandom)
			 * cause EINVAL when used directly.
			 */
			strlcpy(parent, path, sizeof(parent));
			slash = strrchr(parent, '/');
			if ((slash != NULL) && (slash != parent)) {
				*slash = '\0';
			} else if (slash == parent) {
				parent[1] = '\0';
			}
			open_path = parent;
			access = perms_to_landlock_dir(perms);
		}
	} else {
		// Path doesn't exist yet — open parent directory
		strlcpy(parent, path, sizeof(parent));
		slash = strrchr(parent, '/');
		if ((slash != NULL) && (slash != parent)) {
			*slash = '\0';
		} else if (slash == parent) {
			parent[1] = '\0';
		}
		open_path = parent;
		access = perms_to_landlock_dir(perms);
	}

	if (access == 0) {
		return 0;
	}

	fd = open(open_path, O_PATH | O_CLOEXEC);
	if (fd < 0) {
		return -1;
	}

	path_beneath.allowed_access = access;
	path_beneath.parent_fd = fd;

	if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
	                      &path_beneath, 0) != 0) {
		fprintf(stderr, "Sandbox: landlock_add_rule(%s) failed (errno=%d), continuing.\n",
		        path, errno);
		close(fd);
		return -1;
	}

	close(fd);
	return 0;
}

static int apply_landlock(void) {
	int ruleset_fd, i, success_count, abi;
	struct landlock_ruleset_attr ruleset_attr = {
		.handled_access_fs =
			LANDLOCK_ACCESS_FS_READ_FILE |
			LANDLOCK_ACCESS_FS_READ_DIR |
			LANDLOCK_ACCESS_FS_WRITE_FILE |
			LANDLOCK_ACCESS_FS_REMOVE_FILE |
			LANDLOCK_ACCESS_FS_REMOVE_DIR |
			LANDLOCK_ACCESS_FS_MAKE_CHAR |
			LANDLOCK_ACCESS_FS_MAKE_DIR |
			LANDLOCK_ACCESS_FS_MAKE_REG |
			LANDLOCK_ACCESS_FS_EXECUTE,
	};

	abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
	if (abi < 0) {
		fprintf(stderr, "Sandbox: Landlock not supported (kernel too old?), continuing.\n");
		return -1;
	}

	ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
	if (ruleset_fd < 0) {
		fprintf(stderr, "Sandbox: landlock_create_ruleset failed, continuing.\n");
		return -1;
	}

	success_count = 0;
	for (i = 0; i < sandbox_path_count; i++) {
		if (add_landlock_path(ruleset_fd, sandbox_paths[i].path,
		                      sandbox_paths[i].perms) == 0) {
			success_count++;
		}
	}

	if (success_count == 0) {
		fprintf(stderr, "Sandbox: no Landlock rules applied, skipping enforcement.\n");
		close(ruleset_fd);
		return -1;
	}

	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
		fprintf(stderr, "Sandbox: prctl(NO_NEW_PRIVS) failed, continuing.\n");
		close(ruleset_fd);
		return -1;
	}

	if (landlock_restrict_self(ruleset_fd, 0) != 0) {
		fprintf(stderr, "Sandbox: landlock_restrict_self failed, continuing.\n");
		close(ruleset_fd);
		return -1;
	}

	close(ruleset_fd);
	return 0;
}

static int apply_seccomp(void) {
	struct sock_filter filter[] = {
		BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_ptrace, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_process_vm_readv, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_process_vm_writev, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_kexec_load, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

#ifdef __NR_kexec_file_load
		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_kexec_file_load, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_init_module, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_finit_module, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_delete_module, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mount, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_umount2, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_pivot_root, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_reboot, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_swapon, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_swapoff, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_sethostname, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_setdomainname, 0, 1),
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),

		/* Allow everything else */
		BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
	};

	struct sock_fprog prog = {
		.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
		.filter = filter,
	};

	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
		fprintf(stderr, "Sandbox: seccomp filter failed, continuing.\n");
		return -1;
	}

	return 0;
}

int sandbox_apply(void) {
	int result = 0;

	if (apply_landlock() == 0) {
		fprintf(stdout, "Sandbox: Linux Landlock applied.\n");
	} else {
		result = -1;
	}

	if (apply_seccomp() == 0) {
		fprintf(stdout, "Sandbox: Linux seccomp applied.\n");
	} else {
		result = -1;
	}

	return result;
}

/* =========================================================================
 * macOS: Seatbelt (sandbox-exec re-exec)
 * =========================================================================
 */
#elif defined(__APPLE__)

#include <unistd.h>
#include <sys/wait.h>

/* Write an SBPL-safe string, escaping backslashes and double quotes */
static void sbpl_write_path(FILE *fp, const char *path) {
	const char *p;

	for (p = path; *p != '\0'; p++) {
		if ((*p == '"') || (*p == '\\')) {
			fputc('\\', fp);
		}
		fputc(*p, fp);
	}
}

static int generate_seatbelt_profile(int profile_fd) {
	FILE *fp;
	int i;
	const char *path;
	const char *perms;

	fp = fdopen(profile_fd, "w");
	if (fp == NULL) {
		fprintf(stderr, "Sandbox: cannot open seatbelt profile fd, continuing.\n");
		close(profile_fd);
		return -1;
	}

	fprintf(fp, "(version 1)\n");
	fprintf(fp, "(deny default)\n\n");

	fprintf(fp, "(allow process-fork)\n");
	fprintf(fp, "(allow sysctl-read)\n");
	fprintf(fp, "(allow mach-lookup)\n");
	fprintf(fp, "(allow signal)\n\n");

	fprintf(fp, "(allow network*)\n\n");

	fprintf(fp, "(allow file-read-data file-write-data\n");
	fprintf(fp, "  (literal \"/dev/null\")\n");
	fprintf(fp, "  (literal \"/dev/urandom\")\n");
	fprintf(fp, "  (literal \"/dev/random\")\n");
	fprintf(fp, "  (subpath \"/dev/fd\"))\n\n");

	for (i = 0; i < sandbox_path_count; i++) {
		path = sandbox_paths[i].path;
		perms = sandbox_paths[i].perms;

		if ((strchr(perms, 'w') != NULL) || (strchr(perms, 'c') != NULL)) {
			fprintf(fp, "(allow file-read* file-write*\n");
			fprintf(fp, "  (subpath \"");
			sbpl_write_path(fp, path);
			fprintf(fp, "\"))\n");
		} else {
			fprintf(fp, "(allow file-read*\n");
			fprintf(fp, "  (subpath \"");
			sbpl_write_path(fp, path);
			fprintf(fp, "\"))\n");
		}

		if (strchr(perms, 'x') != NULL) {
			fprintf(fp, "(allow process-exec*\n");
			fprintf(fp, "  (subpath \"");
			sbpl_write_path(fp, path);
			fprintf(fp, "\"))\n");
		}
	}

	fprintf(fp, "\n(allow file-read*\n");
	fprintf(fp, "  (subpath \"/usr/lib\")\n");
	fprintf(fp, "  (subpath \"/usr/local/lib\")\n");
	fprintf(fp, "  (subpath \"/System/Library\"))\n");

	fprintf(fp, "\n(allow process-exec*)\n");

	fclose(fp);
	return 0;
}

int sandbox_apply(void) {
	const char *sandboxed;
	char profile_path[256];
	pid_t pid;
	int status, profile_fd;
	extern char ***_NSGetArgv(void);
	extern int *_NSGetArgc(void);
	char **argv;
	int argc, j;
	char **new_argv;

	// Check if already sandboxed
	sandboxed = getenv("HIAWATHA_SANDBOXED");
	if ((sandboxed != NULL) && (strcmp(sandboxed, "1") == 0)) {
		fprintf(stdout, "Sandbox: macOS Seatbelt already active.\n");
		return 0;
	}

	/* Create temp file with unpredictable name to prevent symlink attacks */
	strlcpy(profile_path, "/tmp/hiawatha-sandbox-XXXXXX", sizeof(profile_path));
	profile_fd = mkstemp(profile_path);
	if (profile_fd == -1) {
		fprintf(stderr, "Sandbox: mkstemp failed, continuing without sandbox.\n");
		return -1;
	}

	if (generate_seatbelt_profile(profile_fd) != 0) {
		unlink(profile_path);
		return -1;
	}

	pid = fork();
	if (pid == -1) {
		fprintf(stderr, "Sandbox: fork failed, continuing without sandbox.\n");
		unlink(profile_path);
		return -1;
	}

	if (pid == 0) {
		// Child: re-exec under sandbox
		setenv("HIAWATHA_SANDBOXED", "1", 1);

		// Get original argv from /proc — on macOS we use _NSGetArgv
		argv = *_NSGetArgv();
		argc = *_NSGetArgc();

		// Build sandbox-exec command
		new_argv = malloc((argc + 4) * sizeof(char *));
		if (new_argv == NULL) {
			_exit(1);
		}
		new_argv[0] = "sandbox-exec";
		new_argv[1] = "-f";
		new_argv[2] = profile_path;
		for (j = 0; j < argc; j++) {
			new_argv[j + 3] = argv[j];
		}
		new_argv[argc + 3] = NULL;

		execvp("sandbox-exec", new_argv);
		fprintf(stderr, "Sandbox: sandbox-exec failed, continuing.\n");
		_exit(1);
	}

	// Parent: wait for sandboxed child
	waitpid(pid, &status, 0);
	unlink(profile_path);

	if (WIFEXITED(status)) {
		exit(WEXITSTATUS(status));
	}
	exit(1);
}

/* =========================================================================
 * Other platforms: No-op
 * =========================================================================
 */
#else

int sandbox_apply(void) {
	fprintf(stderr, "Sandbox: no sandbox support on this platform.\n");
	return -1;
}

#endif

#endif
