/* [ʞ] nkvd.c - XDG directory enforcer
 *  ~ lexi hale  <lexi@hale.su>
 *  🄯 GNU AGPL v3
 *  $ cc -fPIC -pie -shared nkvd.c -Wl,-E -onkvd -ldl
 *       [-D_NO_GNU [-D_LIBC=…]] [-D_CONF_HOME=…]
 *  $ export LD_PRELOAD=nkvd.so <dissident>
 *
 *  ! NOTE:  for unknown reasons,  nkvd currently only works
 *    when  built without optimizations.  it's probably that
 *    wrecker Trotsky's fault. i'm working on it.
 *    - partial solution on GCC: using #pragma to disable
 *      optimization around the sensitive code. unclear if
 *      this bug affects other compilers.
 *    - bizarre complication: the bug can only be reproduced
 *      if  the -O flag  itself is present;  turning on  the 
 *      same  individual  optimization flags that  -O<x>  is 
 *      supposed to represent does not trigger the problem.
 *    - conclusion: compiler devils
 *
 *  ! WARNING: test this program thoroughly before you start
 *    exporting it from your shellrcs; if things get fucked,
 *    you  may not be able to execute any binaries with  the
 *    variable set,  and will need to log in as root or boot
 *    off  a rescue disk  to fix the problem.  LD_PRELOAD is
 *    not a toy. you have been warned.
 *
 *  ! WARNING:  while the  code *should*  theoretically work
 *    with  non-GNU  libcs,  i  don't  have  access  to  any
 *    computers running under such a configuration, so i was
 *    only able to test that the compilation process doesn't
 *    explode. there are likely to be bugs.  reports & merge
 *    requests especially are, as always, welcome.
 *
 *  ? tired of programs disrespecting your XDG configuration
 *    and shitting  all over  home sweet ~ ?  fear  no more!
 *    nkvd  is your  very  own digital  thug  to whip  those
 *    dissidents  into  line like  the  counterrevolutionary
 *    scum they  are.  simply command LD to preload nkvd.so,
 *    and it will wiretap startup,  then intercept any calls
 *    to  open(2)  (not  just   fopen)  to  make  sure  they
 *    point where  they're supposed  to. nkvd's  behavior is
 *    controlled by a number of environment variables:
 *
 *      - XDG_CONFIG_HOME  specifies the redirection target,
 *        to,  but can  be  overridden  with nkvd_gulag.  if
 *        neither  are  set,   $HOME/.config  will  be  used
 *        instead.
 *
 *      - nkvd_hq  specifies which directory to protect from
 *        cyberkulaks. it  defaults to your  home directory.
 *        attempts  to access  dotfiles  or dotfolders  with
 *        appropriate  names  in   this  directory  will  be
 *        redirected.
 *
 *      - nkvd_subversives  tells  nkvd  which  programs  to
 *        interdict. it is a  colon-separated list of names.
 *        to determine  if a  program is  on the  List, nkvd
 *        will check the  value of basename(argv[0]) against
 *        its  members. if  nkvd_subversives is  unset, nkvd
 *        will  interdict  all  programs except  for  system
 *        utilities  like rm and ls.  the first character of
 *        nkvd_subversives must be either "+", in which case
 *        it functions as a blacklist,  or a  "-",  in which
 *        case it functions as a whitelist.
 *
 *      - nkvd_interdict_all   tells  nkvd   which   dotfile
 *        accesses   to  redirect.   by  default,   it  will
 *        redirect access  to files  or folders  whose paths
 *        begin with  the string $HOME/.(basename(argv[0])),
 *        instead  returning fds to $XDG_CONFIG_HOME/$1.  if
 *        nkvd_interdict_all  is set,  it will  redirect ALL
 *        accesses   to  files   whose   paths  begin   with
 *        $HOME/.  this  is   a  particularly  extreme  mode
 *        to  operate nkvd  in  and it  is  liable to  cause
 *        serious problems,  in particular for  any programs
 *        attempting   to   access    files   in   ~/.config
 *        or   ~/.local,  another   XDG  directory.   it  is
 *        recommended that nkvd_interdict_all should only be
 *        used with a carefully selected blacklist!
 *
 * NOTE: if you are compiling nkvd.c for a libc other than
 *       glibc, be sure to pass the flag -D_NO_GNU to the
 *       compiler to ensure appropriate behavior at runtime.
 *
 * TODO: extend nkvd to enforce other XDG directories?
 *
 * TODO: build labor  power, arm the workers,  and smash the
 *       bourgeoisie  to   impose  a  dictatorship   of  the
 *       proletariat.
 *
 * TODO: figure out a way to deal with {open,fstat,unlink}at(2)
 *
 * TODO: do a better job of canonicalizing paths, in case
 *       anyone tries to be tricky. (alas, realpath() et al
 *       are not viable for these purposes as they only work
 *       for files already in existence.)
 *
 * TODO: -pie allows the  program to be run  as a standalone
 *       binary.  exploit this;  allow  it  to launch  other
 *       binaries with itself  as LD_PRELOAD? detecting this
 *       state  of  affairs  will likely  only  be  possible
 *       through checking if LD_PRELOAD is empty. due to the
 *       difficulty of determining a binary's path on linux,
 *       this may  not be  feasible. switch back  to -shared
 *       and delete -Wl,-E if so.
 *
 * TODO: support a mappings file/variable for those programs
 *       too special to simply name their config file
 *       "~/.(argv[0])". 
 *
 * TODO: exempt xdg dirs beginning with ~/. from proscription
 *       in nkvd_interdict_all=1 mode
 *
 * TODO: instead of function passthrough,  alter environment
 *       to delete  LD_PRELOAD and re-exec  whitelisted apps
 *       without nkvd loading at all
 */

#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <pwd.h>
#ifdef _NO_GNU
	/* the POSIX version of basename is a goddamn mess and
	 * it's better to use the glibc version where possible. */
#	include <libgen.h>
#	define SYMSRC hnd
#else
	/* glibc's basename() is imported from <string.h> via
	 * the following directive. i have no idea how this
	 * magic works unless they're abusing asm declarations */
#	define _GNU_SOURCE /* for basename() */
#	define __USE_GNU   /* for RTLD_NEXT  */
#	ifdef _USE_RTLD_NEXT
#		define SYMSRC RTLD_NEXT
#	else
#		define SYMSRC hnd
#	endif
#endif
#include <string.h>
#include <dlfcn.h>

#ifndef _LIBC
#	define _LIBC "libc.so.6"
#endif

#ifndef _CONF_HOME
#	define _CONF_HOME ".config"
#endif

#define pstr(x) x,sizeof x
#define hl(txt) "\x1b[1;31m" txt "\x1b[m"
#define bold(txt) "\x1b[1m" txt "\x1b[21m"
#define e_fatal hl("(nkvd fatal)") " "
#define fail(code, err) { write(2,pstr(e_fatal " " err)); _exit(code); }
#define dbg(x) write(2,pstr(bold("(nkvd)") " " x))

typedef _Bool bool;
enum { false = 0, true = 1 };

static bool intercept;
static int wiretap_argc = 0;
static const char** wiretap_argv = NULL;
static const char* redir_prefix;
static const char* redir_to;
static size_t redir_len;
static const char* hq;
static size_t hq_len;

static void
configdirs(void) {
	const char* nkvd = getenv("nkvd_gulag");
	if (nkvd != NULL) redir_to = nkvd, redir_len = strlen(nkvd);

	const char* xdg = getenv("XDG_CONFIG_HOME");
	if (xdg != NULL) redir_to = xdg, redir_len = strlen(xdg);

	const char* home = getenv("HOME");
	if (home == NULL) {
		const char* user = getenv("USER");
		struct passwd* p;
		if (user == NULL) {
			uid_t u = geteuid();
			p = getpwuid(u);
		} else 
			p = getpwnam(user);
		if(p == NULL) fail(-5, "XDG_CONFIG_HOME, HOME, and USER "
				"environment variables are all unset, and no home "
				"directory can be identified for your user ID. "
				"something is seriously wrong with your system, "
				"comrade. bailing.");
		home = p -> pw_dir;
	}
	size_t homelen = strlen(home);

	const char* h = getenv("nkvd_hq");
	if (h != NULL) hq = h, hq_len = strlen(h);
		else hq = strdup(home), hq_len = homelen;

#	define CONFDIR "/" _CONF_HOME "/"
	char* buf = malloc(homelen + sizeof CONFDIR);
	/* since this will be used throughout the lifetime of the
	 * program, we don't need to free it, and it would actually
	 * just slow down the exit process to do so. */
	strcpy(buf, home);
	strcpy(buf + homelen, CONFDIR);
	redir_len = homelen + sizeof CONFDIR;
#	undef CONFDIR

	redir_to = buf;
}

static bool
checkpath(const char* path, size_t len, char* gulag) {
	char c[hq_len + len + 4];
	strncpy(c, hq, hq_len);
	c[hq_len] = '/';
	c[hq_len+1] = '.';
	c[hq_len+2] = 0;
	
	const char* all = getenv("nkvd_interdict_all");
	size_t clen;
	if (all == NULL || !(all[0] == '1' && all[1] == 0)) {
		strcpy(c + hq_len + 2, wiretap_argv[0]);
		clen = strlen(c);
		c[clen] = '/'; c[clen+1] = 0;
		if (len > clen) ++clen;
	} else {
		clen = strlen(c);
	}
		
	if (strncmp(path, c, clen) == 0) {
		/* guilty as sin, your honor */
		strncpy(gulag,redir_to,redir_len);
		strncpy(gulag + redir_len - 1,
				path + hq_len + 2,
				len - (hq_len + 1));
		return true;
	} else {
		/* no further questions, citizen */
		return false;
	}
}

static bool
interrogate(const char* path, size_t plen, char* gulag) {
	/* papers, please */
	if (path[0] != '/') for (size_t i = 64; i<PATH_MAX; i << 1) {
		char cwd[i + plen + 1];
		if (getcwd(cwd, i) == NULL) continue;
		size_t cwdlen = strlen(cwd);
		cwd[cwdlen] = '/';
		strncpy(cwd + cwdlen + 1, path, plen + 1);
		return checkpath(cwd,cwdlen + plen, gulag);
	}
	return checkpath(path, plen, gulag);
}

static bool
shitlist(const char* name) {
#	ifdef _NO_GNU /* avoid POSIX weirdness */
		{ char* name = strdup(name); /* yay shadowing! */
#	endif

	const char* base = basename(name);

	const char* list = getenv("nkvd_subversives");
	if (list == NULL) return true;

	if (list[0] != '+' && list[0] != '-') 
		fail(-4, "the variable $nkvd_subversives needs to "
				"begin with either a + to indicate blacklist "
				"mode, or a - to indicate whitelist mode.");

	const enum { blacklist, whitelist } mode =
		(list[0] == '+' ? blacklist : whitelist);

	const char* p = list + 1;
	for(;;) {
#		ifndef _NO_GNU
			const char* end = strchrnul(p, ':');
#		else
			const char* end = strchr(p, ':');
			if (end == NULL) end = p + strlen(p);
#		endif

		if(strncmp(base,p,end-p) == 0) return (mode == blacklist);

		if (*end == 0) break;
			else p = end + 1;
	}

#	ifdef _NO_GNU
		free(name); }
#	endif
	return (mode == whitelist);
}

#pragma GCC push_options
#pragma GCC optimize ("O0")

#define STAT_PARAMS  const char* volatile path, struct stat* volatile statbuf
#define XSTAT_PARAMS int volatile ver, const char* volatile path, struct stat* volatile statbuf
#define decl_stat_ptr(nm)  static int (*libc_##nm) (STAT_PARAMS)
#define decl_xstat_ptr(nm) static int (*libc_##nm) (XSTAT_PARAMS)

static int (*libc_open)   (const char* volatile,int,...);
static int (*libc_unlink) (const char* volatile);
static int (*libc_access) (const char* volatile,int);
static int (*libc_eaccess)(const char* volatile,int);
static int (*libc_rename) (const char* volatile,const char* volatile);

decl_stat_ptr(stat);
decl_stat_ptr(stat64);
decl_stat_ptr(lstat);
decl_stat_ptr(lstat64);

#ifndef _NO_GNU
	decl_xstat_ptr(xstat);
	decl_xstat_ptr(xstat64);
	decl_xstat_ptr(lxstat);
	decl_xstat_ptr(lxstat64);
#endif

static int
nkvd_stat(int (*volatile fn)(STAT_PARAMS), STAT_PARAMS) {
	if (intercept) {
		size_t plen = strlen(path);
		char gulag[redir_len + plen];
		if (interrogate(path, plen, gulag)) {
			return (*fn)(gulag,statbuf);
		}
	}
	return (*fn)(path,statbuf);
}

static int
nkvd_xstat(int (*volatile fn)(XSTAT_PARAMS), XSTAT_PARAMS) {
	if (intercept) {
		size_t plen = strlen(path);
		char gulag[redir_len + plen];
		if (interrogate(path, plen, gulag)) {
			return (*fn)(ver,gulag,statbuf);
		}
	}
	return (*fn)(ver,path,statbuf);
}

#define def_stat_fn(nm,fn) int nm(STAT_PARAMS) { return nkvd_stat(libc_##fn,path,statbuf); }
#define def_xstat_fn(nm,fn) int nm(XSTAT_PARAMS) { return nkvd_xstat(libc_##fn,ver,path,statbuf); }

def_stat_fn(stat,stat);
def_stat_fn(stat64,stat64);
def_stat_fn(lstat,lstat);
def_stat_fn(lstat64,lstat64);

#ifndef _NO_GNU
	def_xstat_fn(__xstat,xstat);
	def_xstat_fn(__xstat64,xstat64);
	def_xstat_fn(__lxstat,lxstat);
	def_xstat_fn(__lxstat64,lxstat64);
#endif

#pragma GCC pop_options

int unlink(const char* volatile path) {
	if (intercept) {
		size_t plen = strlen(path);
		char gulag[redir_len + plen];
		if (interrogate(path, plen, gulag)) {
			return (*libc_unlink)(gulag);
		}
	}
	return (*libc_unlink)(path);
};

typedef int(*intfn)(const char*,int);
static int
nkvd_intcall(intfn fn, const char* volatile path, int mode) {
	if (intercept) {
		size_t plen = strlen(path);
		char gulag[redir_len + plen];
		if (interrogate(path, plen, gulag)) {
			return (*fn)(gulag,mode);
		}
	}
	return (*fn)(path,mode);
};

int access (const char* volatile path, int mode)
	{ nkvd_intcall(libc_access,path,mode); }

int eaccess (const char* volatile path, int mode)
	{ nkvd_intcall(libc_eaccess,path,mode); }

#define max(a,b) ((a) > (b) ? (a) : (b))
int rename(const char* volatile from,
           const char* volatile to) {
	if (intercept) {
		const size_t fromlen = strlen(from),
		               tolen = strlen(to);
		char fromgulag[redir_len + tolen  ],
		       togulag[redir_len + fromlen];

		const char* fromfinal,
		          * tofinal;

		if (interrogate(from, fromlen, fromgulag))
			fromfinal = fromgulag;
		else fromfinal = from;

		if (interrogate(to, tolen, togulag))
			tofinal = togulag;
		else tofinal = to;

		return (*libc_rename)(fromfinal,tofinal);
	}

	return (*libc_rename)(from,to);
};

int open(const char* volatile path, int flags, ...) {
	/* fuck you for using varargs, asshole */
	va_list v; va_start(v,flags);
	mode_t m;
	if(flags & O_CREAT) m = va_arg(v, mode_t);
	va_end(v);

	if (intercept) {
		size_t plen = strlen(path);
		char gulag[redir_len + plen];
		if (interrogate(path, plen, gulag)) {
			return (*libc_open)(gulag, flags, m);
		}
	}
	return (*libc_open)(path, flags, m);
}

#include <stdio.h>
int nkvd_init(int argc, const char** argv) {
	wiretap_argc = argc;
	wiretap_argv = argv;

#	ifndef _USE_RTLD_NEXT
		/* RTLD_NEXT is buggy as hell so we avoid it by default */
		void* SYMSRC = dlopen(_LIBC,RTLD_LAZY);
		if (SYMSRC == NULL)
			fail(-2,"compiled in non-RTLD_NEXT mode and cannot load "
				"libc from " bold(_LIBC) "; please recompile "
				" and specify your libc with -D_LIBC= on the "
				"compile command line.");
#	endif

#	define load_fn(fn,pfx) libc_##fn = dlsym(SYMSRC, #pfx #fn)
		load_fn(open,);      load_fn(unlink,);
		load_fn(access,);    load_fn(eaccess,);
		load_fn(rename,);
		load_fn(stat,);      load_fn(lstat,);
		load_fn(stat64,);    load_fn(lstat64,);
		
#		ifndef _NO_GNU
			load_fn(xstat,__);   load_fn(lxstat,__);
			load_fn(xstat64,__); load_fn(lxstat64,__);
#		endif
#	undef load_fn
#	define dump(v) printf("-- " #v ": %p", libc_##v)
	dump(lstat);
	dump(lstat64);
	dump(lxstat);
	dump(lxstat64);
	
	/* have enough stat fns been found for us to proceed? */
	bool statfns = (  (libc_stat    && libc_lstat)
	               || (libc_stat64  && libc_lstat64)
#ifndef _NO_GNU
	               || (libc_xstat   && libc_lxstat)
	               || (libc_xstat64 && libc_lxstat64)
#endif
	               );

	if (! (libc_open && statfns && libc_unlink &&
	       libc_access && libc_eaccess && libc_rename))
		fail(-3, "your libc is defective. bailing.");

	intercept = shitlist(argv[0]);
	if (intercept) {
		configdirs();
	}

	return 0; /* ?? why is this int, not void */
}

__attribute__((section(".init_array"))) static void* nkvd_constructor = &nkvd_init;

int main() { return 0; } /* stub for -pie */
